- PostgreSQL is the only configured database engine.
- The API uses
decision_reader, a separate login role from the schema owner. decision_readerisNOSUPERUSER,NOCREATEDB,NOCREATEROLE, andNOINHERIT.- The migration grants the reader
SELECTon the demo tables and no write privileges. - Schema bootstrap, migrations, and seed use the admin connection and are separate Compose jobs.
- M1 itself calls no LLM and exposes no public query execution endpoint. M2's optional provider path is internal and remains proposal-only.
SqlCandidateis untrusted input; it contains no authorization state.QueryPlanis created only after deterministic policy and EXPLAIN/cost acceptance.QueryExecutionis the bounded outcome of an accepted plan; proposal, authorization, and execution are never treated as the same state.- M2 provider output is converted to
SqlCandidate(source=LLM)before any M1 call; providers cannot access the reader engine, admin connection, or credentials. - Health performs
SELECT 1through the configured reader connection. - Candidate SQL is parsed with
sqlglotusing the PostgreSQL dialect and exactly one statement is required. - Only
SELECTASTs, including SELECT-based CTEs, are accepted. - Tables must exist in the explicit queryable catalog; non-public schemas, unknown tables, and PostgreSQL system catalogs are rejected.
customers.external_keyis marked internal and non-queryable; wildcard projections that would expose it are rejected.- PostgreSQL functions use an analytical allowlist.
- EXPLAIN runs only after AST/object policy acceptance and before candidate execution.
- The supported service API is
plan(SqlCandidate)followed byexecute(QueryPlan). Raw SQL, rejected plans, and copied/foreign plans are rejected at the execution boundary. - Context sent to M2 providers is derived from queryable catalog metadata only. Non-queryable columns such as
customers.external_key, forbidden catalogs, hidden objects, sample values, and secrets are excluded. - EXPLAIN and execution use the reader connection in a read-only transaction.
- Plan rows, plan cost, returned rows, and statement timeout are bounded by settings.
- Tracing has bounded stage spans and does not record result rows or full candidate SQL.
QueryIntent is not an authorization artifact. It is an untrusted model
interpretation of the bounded schema context. A valid-looking intent cannot
authorize a write, a hidden table, or a sensitive column. The SQL proposal is
still converted to SqlCandidate(source=LLM) and must pass the unchanged M1
parse, AST policy, reader validation, EXPLAIN, cost gate, and execution path.
M2.5 intentionally has no SQL repair loop: one intent call and one SQL call are made, and any resulting candidate is handled by M1 or returned as a typed failure.
The following are intentionally not claimed as implemented:
- Server-owned tenant predicates and authorization policy evaluation beyond the application catalog.
- PostgreSQL RLS where appropriate.
- Result semantic validation and answer grounding.
- Full Jaeger deployment/exporter verification.
These controls are deterministic code and are covered by unit and Docker-backed PostgreSQL adversarial tests. Prompt instructions are not a security control. M1 accepts candidate SQL directly; M2-generated candidates still pass through the same M1 control plane.
Compose credentials are development-only example values. Production secrets must come from the deployment secret/configuration system and must never be committed.