Skip to content

Merge security + reliability PRs (11): explain SQLi, dep CVEs, pool fixes, introspection - #2

Merged
cupskeee merged 30 commits into
mainfrom
merge/security-reliability-batch
Sep 6, 2026
Merged

cupskeee merged 30 commits into
mainfrom
merge/security-reliability-batch

Conversation

@cupskeee

@cupskeee cupskeee commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Integrates worthwhile upstream improvements after the security work, weighted to a read-only, local-stdio deployment. Checks: ruff clean, pyright 0 errors, pytest tests/unit = 208 passed / 27 skipped.

  • Security/DX: SQL-injection prevention in the explain tool; dependency + base-image bumps past known CVEs; schema comments + materialized-view introspection; allow timezone() in restricted mode.
  • Reliability: connection-pool fixes (no churn on bad SQL, zero idle connections, session-state reset between requests).
  • Small wins: clearer errors, agent docs, modern type hints, README notes.

🤖 Generated with Claude Code

valdezm and others added 30 commits May 14, 2025 11:44
AGENTS.md provides universal rules for all AI agents: safety guardrails
(get_sql_driver() hard rule), CI commands, project structure, and code
conventions. CLAUDE.md adds Claude-specific tool preferences, MCP-aware
validation workflows, and commit style guidance — referencing AGENTS.md
for shared rules rather than duplicating them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ools

Add PostgreSQL COMMENT support to list_objects and get_object_details:
- list_objects: include obj_description() for tables, views, and materialized views
- get_object_details: include col_description() for column comments and
  obj_description() for table/view/materialized view comments

Add materialized view as a new object_type:
- list_objects: query pg_class with relkind='m' for materialized views
- get_object_details: query pg_attribute for columns (since materialized
  views are not in information_schema.columns)

This enables LLMs using the MCP server to discover schema documentation
stored as COMMENT ON TABLE/VIEW/COLUMN statements, which is the standard
PostgreSQL mechanism for schema documentation.

Closes crystaldba#71
These pg_catalog functions are read-only and needed for the schema
comment feature to work in restricted mode.
Add _validate_explain_input() using pglast to parse and validate SQL before f-string concatenation. Prevents multi-statement injection and restricts EXPLAIN ANALYZE to SELECT-only.
17 unit tests + 5 integration tests covering valid queries, multi-statement injection rejection, empty/invalid input, EXPLAIN ANALYZE DML restrictions, and COPY exfiltration prevention.
Add "PostgreSQL" keyword to all tool descriptions so LLM search
can find them. Make get_object_details and list_objects descriptions
explicit about returned data (columns, comments, constraints, indexes).
Replace legacy typing imports (List, Dict, Optional, Union) with
built-in generics (list, dict, X | None, X | Y) across 7 source
files. Removes the UP006/UP035/UP045 ruff ignore entries and
the TODO comment referencing issue crystaldba#129.
Establish the connection pool on first tool use instead of at server
startup, and let idle connections be reaped, so defining many database
MCP configs no longer means every server holds an open Postgres
connection from session start.

- server.py: store the database URL without connecting eagerly; the
  lazy path in SqlDriver.execute_query opens the pool on first use.
- sql_driver.py: pool now uses min_size=0 with a max_idle reaper so
  unused/idle connections release.
- Add --max-idle flag and DATABASE_MAX_IDLE env var (default 300s) to
  tune the idle timeout; invalid values fall back to the default via a
  validating max_idle property.
- README: document lazy connections and the idle timeout with examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two defects in DbConnPool/SqlDriver combined to produce the behavior
reported in crystaldba#98 (ever-incrementing pool-N names, 'the pool is closed'
errors, and memory growth proportional to usage):

1. SqlDriver.execute_query marked the shared pool invalid on *every*
   exception, including query-level errors like bad SQL or a statement
   timeout. The next call then tore down and rebuilt the pool, so each
   malformed query churned a pool. Now only connection-level errors
   (OperationalError/InterfaceError, excluding QueryCanceled) invalidate
   the pool.

2. DbConnPool.pool_connect had no synchronization: concurrent callers
   that both observed an invalid pool each built an AsyncConnectionPool,
   and every overwritten pool was left open forever - leaking its
   connections and worker tasks - while callers holding a pool that a
   later winner closed failed with 'the pool ... is closed'. Pool
   (re)creation is now serialized with an asyncio.Lock and re-checked
   under the lock.

Also pass check=AsyncConnectionPool.check_connection so connections
broken while idle (server restart, idle timeout) are discarded on
checkout instead of surfacing as query errors that would previously
have poisoned the pool.

In production we observed this as steady ~6MiB/day memory growth in a
long-lived streamable-http deployment, resetting only on pod restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
uv lock --upgrade across the board, constraining mcp to <2 (2.0 removed
mcp.server.fastmcp, which server.py is built on; 1.29.0 is the newest 1.x
and satisfies every advisory). Base image bookworm -> trixie for both
stages, and the runtime stage upgrades the base image's pip, which ships
with known CVEs despite never being used at runtime.

Headline bumps: aiohttp 3.11.16->3.14.3, cryptography 46.0.3->50.0.0,
starlette 0.46.1->1.3.1, h11 0.14->0.16, pyjwt 2.10.1->2.13.0,
urllib3 2.3.0->2.7.0, python-multipart 0.0.21->0.0.32.

Test suite passes (206 passed, 9 skipped, 1 xfailed); streamable-http
transport smoke-tested against the built image.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…indexes

Pooled backends reused by execute_query kept session GUCs after COMMIT
and leftover HypoPG indexes after hypothetical-index analysis. Later
MCP requests that hit the same PID inherited that state.
Give AsyncConnectionPool a reset callback that runs DISCARD ALL outside
a transaction and, if HypoPG is installed, schema-qualified hypopg_reset().
Cleanup errors propagate so the pool discards the connection.
Combine DTA hypopg_create_index and hypopg_list_indexes into one
checkout so size estimates still work after reset-on-return.

Closes crystaldba#203
…rror-classification + check_connection)
… line length)

Merging crystaldba#173 (which removed the UP006/UP035 ruff ignores) together with PRs that
still used legacy typing (`List`/`Dict`, `typing.List`) left the tree failing ruff.
Applies crystaldba#173's modernization to the stragglers (test_dta_calc.py, tests/utils.py,
conftest.py, index_opt_base.py) and wraps three long tool-description strings from
crystaldba#160 (E501) via implicit string concatenation (text unchanged).

ruff clean, pyright 0 errors, pytest 208 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6w353jusev2fgmWQY1mS1
@cupskeee
cupskeee merged commit 197e970 into main Sep 6, 2026
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.