Skip to content

fix(run_sql): scope agent SQL to the finance tables with a Postgres role - #58

Merged
IBJunior merged 1 commit into
mainfrom
claude/brave-ramanujan-0fcca1
Nov 8, 2026
Merged

IBJunior merged 1 commit into
mainfrom
claude/brave-ramanujan-0fcca1

Conversation

@IBJunior

@IBJunior IBJunior commented Nov 8, 2026

Copy link
Copy Markdown
Member

Problem

run_sql ran agent-written SQL as the app's own database user, inside a READ ONLY transaction. That blocked writes but not reads, so SELECT auth_tokens, env, headers FROM mcp_server worked. That table holds plaintext MCP OAuth tokens, stdio env vars and HTTP headers. The checkpoint tables (every conversation), config and thread were readable too. MCP tools auto-approve, so text injected into an MCP tool result could get the agent to read those secrets and send them out through another tool, with no approval card along the way.

Fix

Postgres now decides what run_sql can read; there is no table filter in code.

  • Migration 0006_analytics_readonly_role.sql (hand-written, idempotent): creates a cameron_analytics role (NOLOGIN) with USAGE on public and SELECT on transaction and category only, then grants the app user membership in it.
  • runReadOnlyQuery runs SET LOCAL ROLE cameron_analytics inside the existing transaction. READ ONLY, statement_timeout, the rollback and the row cap are unchanged.
  • Closing the set_config escape. SET ROLE changes only the current role, and set_config('role', 'none', true) is SET ROLE as a function call, which the guard's SET keyword check misses. Paired with query_to_xml('select … from mcp_server', …), a single SELECT read mcp_server on the dev database. The migration therefore revokes EXECUTE on set_config from PUBLIC and grants it back to the app user. It ends with a check that raises if the role can still call set_config or read mcp_server, because a REVOKE run by a non-owner only warns. Migrations must run as a superuser; all three compose stacks already do.
  • Agent-facing error: a permission denial (42501) comes back as { error: "That table isn't available to run_sql…", availableTables, detail }. Other DB errors keep the existing Query failed: shape. The tool description is unchanged.

Tests

  • pnpm test (459) and tsc --noEmit pass.
  • New unit tests cover the denial mapping and confirm that other errors aren't treated as denials.
  • A drift test reads drizzle/*.sql and checks that the role's grants match the tables describe_finance_schema documents, with nothing beyond SELECT and no whole-schema grants.
  • Live on the dev DB (mydb_dev, 5544), through the real runSql:
    • A finance aggregate gives the same result as before.
    • mcp_server, checkpoints, config, thread, the drizzle schema and the set_config escape are all refused.
    • Re-running the migration SQL succeeds.
  • The eval sandbox (5545) migrates cleanly and has the expected grants.
  • Production was not touched. Its migrate image copies drizzle/ and runs as the superuser, so it picks this up on the next --profile full up.

Docs

The run_sql, raw-SQL boundary and config bullets in AGENTS.md, plus a "What run_sql can read" subsection in docs/ARCHITECTURE.md.

🤖 Generated with Claude Code

run_sql ran agent-written SQL as the app's own DB user, so a SELECT could read
mcp_server (plaintext OAuth tokens, env vars, headers), the checkpoint tables,
config and thread. MCP tools auto-approve, so an injected tool result could get
the agent to read those and pass them on.

- Migration 0006 creates cameron_analytics (NOLOGIN) with SELECT on
  transaction and category only; runReadOnlyQuery switches to it with
  SET LOCAL ROLE inside the existing READ ONLY transaction.
- Revoke set_config from PUBLIC: set_config('role', ...) is SET ROLE as a
  function and, with query_to_xml, escaped the role in one SELECT. The
  migration raises if the revoke did not take.
- A permission denial (42501) returns an "isn't available to run_sql"
  payload with availableTables; other errors keep their shape.
- Tests pin the error mapping and that the grants match the tables
  describe_finance_schema documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IBJunior
IBJunior merged commit 8a4dd87 into main Nov 8, 2026
1 check passed
@IBJunior
IBJunior deleted the claude/brave-ramanujan-0fcca1 branch November 8, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant