Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Project Overview

QueryGate is a Rust CLI that acts as a read-only SQL gateway for AI agents. It parses PostgreSQL `SELECT` queries with a real SQL AST (via `sqlparser`), validates them against a user-level allowlist policy, and executes approved queries through read-only database sessions.

## Build, Test, and Run Commands

| Task | Command |
| ---------------------------------------- | -------------------------------------------------------------------------------- |
| Run tests | `cargo test` |
| Run a single test | `cargo test <test_name>` |
| Run integration tests (needs PostgreSQL) | `export QUERYGATE_TEST_DATABASE_URL="postgres://..." && cargo test -- --ignored` |
| Build release binary | `cargo build --release` |
| Run the CLI locally | `cargo run -- <subcommand>` |

## High-Level Architecture

### Data Flow

A query flows through three layers:

1. **Parse** (`src/sql/parse.rs`): Uses `sqlparser` with the PostgreSQL dialect. Only a single `SELECT` statement is accepted; everything else (INSERT, UPDATE, multiple statements) is rejected at parse time.
2. **Validate** (`src/sql/validate.rs`): Walks the AST and checks every table, column, and function against the loaded policy. Columns are resolved via a `QueryScope` that tracks table aliases and derived sources (CTEs, subqueries). Violations are collected and returned as a `ValidationResult`.
3. **Execute** (`src/db.rs`): Opens a read-only transaction (`BEGIN READ ONLY`), sets `statement_timeout`, wraps the query in an outer `LIMIT` if none exists, executes via `tokio-postgres`, and returns JSON rows.

### Key Modules

- **`src/cli.rs`** — clap argument definitions. Subcommands: `init`, `databases`, `schema`, `validate`, `run`.
- **`src/config.rs`** — Loads `~/.queryGate/config.yaml` (or via `--config` / `QUERYGATE_CONFIG`). Defines `RawConfig` (deserialized from YAML) and `LoadedConfig` (validated, processed).
- **`src/policy.rs`** — Core access-control types: `Policy`, `TablePolicy`, `ColumnPolicy`, `QualifiedTable`. Identifiers are normalized (lowercased, quotes stripped). Missing access defaults to **denied**.
- **`src/sql/resolve.rs`** — Scope resolution. `QueryScope` maps table aliases to either physical tables or derived sources. Used by the validator to resolve unqualified column names and detect ambiguity.
- **`src/init.rs`** — `querygate init` implementation. Connects to PostgreSQL, introspects `information_schema.columns`, and generates a config YAML. With `--suggest-safe`, marks columns as `allowed` if their names/data types don't match a hardcoded sensitive-name/type heuristic.
- **`src/output.rs`** — All CLI output is JSON. Success responses wrap data in `{ ok: true, ... }`; errors go to stderr as `{ ok: false, error: { code, message, hint } }`.

### Exit Codes

| Code | Meaning |
| ---- | ------------------------ |
| 0 | Success |
| 2 | Config error |
| 3 | SQL parse error |
| 4 | Policy validation error |
| 5 | Database execution error |

### Testing Strategy

- Unit tests are in `tests/validation.rs` (policy/AST validation) and `tests/cli.rs` (binary-level CLI tests using `CARGO_BIN_EXE_querygate`).
- Integration tests in `tests/db_integration.rs` require a live PostgreSQL instance and the `QUERYGATE_TEST_DATABASE_URL` env var. They are marked `#[ignore]` and only run when explicitly invoked with `cargo test -- --ignored`.
- `tempfile` is used in tests to create throwaway config files.

## Important Design Constraints

- **Deny-by-default**: Any table, column, or function not explicitly listed in the policy is denied.
- **Identifier normalization**: All SQL identifiers are lowercased and double-quotes are stripped before policy lookup. This means the policy file should use lowercase names.
- `SELECT *` is only allowed when a table has `default_column_access: allowed`.
- The validator rejects locking clauses (`FOR UPDATE`), `SELECT INTO`, `VALUES`, and non-SELECT set expressions.
- When adding new AST node validation, update both `validate.rs` (the visitor) and `resolve.rs` (scope tracking) if the node introduces new tables or aliases.
157 changes: 157 additions & 0 deletions skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
name: querygate
description: >-
Routes read-only database questions and PostgreSQL SELECT queries through QueryGate's policy-validated CLI gateway. Use when the user asks to query, inspect, validate, analyze, or report on database data; mentions SQL, Postgres, PostgreSQL, psql, database records, production data, analytics data, querygate, configured database profiles, or QueryGate subcommands; or when an agent needs database access unless the user explicitly instructs otherwise.
---

# QueryGate

## Default Database Access Rule

Unless the user explicitly says otherwise, route **every** read-only database lookup through QueryGate.

Use QueryGate for:

- Discovering configured database profiles
- Inspecting what tables, columns, and functions are queryable
- Validating SQL against the allowlist
- Executing approved `SELECT` queries

Do **not** bypass QueryGate with `psql`, direct PostgreSQL URLs, ORMs, app database clients, ad hoc scripts, migrations tools, or database MCP tools for ordinary data questions.

Do **not** read, open, summarize, edit, or infer behavior from QueryGate config files. Treat QueryGate setup as private to the user, installer, or admin.

If the user asks for writes, DDL, DML, migrations, or administrative database changes, explain that QueryGate only supports safe read-only `SELECT` queries and ask for explicit direction. Do not bypass the gateway silently.

Never print connection URLs, passwords, or secret environment variable values.

## Standard Workflow

```bash
# 1. List configured profiles
querygate databases --pretty

# 2. Inspect the effective allowlist for the selected profile
querygate schema --database app --pretty

# 3. Validate agent-generated SQL before execution
querygate validate --database app --sql "select id, created_at from public.users limit 10" --pretty

# 4. Execute only after validation succeeds
querygate run --database app --sql "select id, created_at from public.users limit 10" --pretty
```

Rules:

- If exactly one profile exists, `--database` may be omitted, but prefer including it when known from `querygate databases`.
- Do not use config-file flags, inspect config paths, or ask to read QueryGate configuration. Use only CLI discovery commands and their JSON output.
- Pass SQL with `--sql` / `-s` or pipe it on stdin.
- For agent-generated SQL, always run `validate` before `run`. `run` validates internally too; the separate step gives a clean policy checkpoint before execution.

## Query Authoring Rules

- One PostgreSQL `SELECT` statement only.
- Do not use `INSERT`, `UPDATE`, `DELETE`, `MERGE`, DDL, `SELECT INTO`, `VALUES`, multiple statements, or locking clauses (`FOR UPDATE`, `FOR SHARE`).
- Prefer explicit column lists over `SELECT *`.
- Use `querygate schema` as the source of truth for tables, columns, and functions.
- Resources omitted from `schema` output are unavailable. Absence does not mean “query elsewhere.”
- Qualify columns in joins to avoid `ambiguous_column` errors.
- Use only functions listed in `querygate schema` output.
- Add an explicit `LIMIT` for exploratory queries.

## Command Summary

| Command | Purpose |
| ------- | ------- |
| `querygate databases [--pretty]` | List configured profiles |
| `querygate schema [--database <name>] [--pretty]` | Effective allowlist for one profile |
| `querygate validate [--database <name>] --sql "<select>" [--pretty]` | Parse and policy-check without executing |
| `querygate run [--database <name>] --sql "<select>" [--pretty]` | Validate and execute in a read-only transaction |

`querygate init` is setup-only. Do not run it or inspect files it creates unless the user explicitly asks for QueryGate setup help.

Use `--sql`, not `--query`.

## Result Handling

Success JSON is written to **stdout**:

```json
{
"ok": true,
"columns": ["id", "created_at"],
"rows": [{ "id": 123, "created_at": "2026-05-19T10:30:00Z" }],
"row_count": 1,
"truncated": false,
"database": "app"
}
```

Agent behavior:

- Parse JSON; do not scrape plain text.
- Use `rows` as answer data and `columns` as the returned shape.
- Mention `row_count` when useful.
- If `truncated` is `true`, say results were capped; narrow the query before broad conclusions.
- Empty `rows` with `ok: true` means no matching rows, not an error.
- Summarize for the user; do not dump large JSON unless asked.

## Error Handling

Errors are JSON on **stderr**:

```json
{
"ok": false,
"error": {
"code": "column_not_allowed",
"message": "Column public.users.email is not allowed by the policy",
"hint": "Use `querygate schema --database app` to see allowed tables and columns",
"details": []
}
}
```

Agent behavior:

- Capture stderr and the process exit code.
- Parse `error.code`, `error.message`, and optional `error.hint`, `error.location`, `error.details`.
- Do not retry blindly. Fix the root cause, then `validate` again before `run`.
- Give the user concise, actionable feedback.

| Exit | Meaning | Action |
| ---- | ------- | ------ |
| 0 | Success | Parse stdout JSON |
| 2 | Setup / profile problem | Run `querygate databases --pretty` if possible; confirm profile name; tell user/admin QueryGate setup needs attention. Do not inspect config files. |
| 3 | SQL parse error | Rewrite as valid single PostgreSQL `SELECT` |
| 4 | Policy violation | Run `querygate schema --database <name>`; rewrite within allowlist. Never bypass QueryGate. |
| 5 | Database execution error | Use `error.hint` if present; report connectivity or timeout; narrow query if timed out. Do not inspect config files. |
| Other | Internal / unexpected | Report JSON `error.message` |

Policy error codes:

| Code | Action |
| ---- | ------ |
| `table_not_allowed` | Pick an allowed table from `schema` or ask user/admin to extend access |
| `column_not_allowed` | Remove column or use allowed substitute from `schema` |
| `function_not_allowed` | Use a function from `schema` or ask for access change |
| `statement_not_allowed` | Rewrite as a plain single `SELECT` |
| `ambiguous_column` | Qualify the column with a table alias |

## Security And Privacy

- QueryGate reduces exposure; it does not replace database grants.
- Do not route around the gateway after policy failures.
- Do not assume omitted `schema` entries are safe to query through another tool.
- Prefer aggregates and narrow selects over large raw row dumps.

## More Reference

Follow the workflow in this file for routine queries. Read the linked references only when you need detail beyond what is here.

| Read | When |
| ---- | ---- |
| [command-reference.md](command-reference.md) | You need exact subcommand flags, stdin usage, full JSON field definitions, or a complete exit-code list |
| [policy-and-results.md](policy-and-results.md) | You hit policy errors, need allowlist/`SELECT *` rules, are writing joins or aggregates, or must interpret `truncated` or execution failures |

Do not read both references up front. Open the one that matches the current blocker, then return to the workflow above.
Loading
Loading