From 59990745c507cecaf9d468a868bbdafbce87573c Mon Sep 17 00:00:00 2001 From: orgoldfus Date: Sun, 24 May 2026 12:20:00 +0300 Subject: [PATCH 1/3] Add QueryGate agent skill for safe database access. Teaches agents to route read-only queries through the CLI, discover allowlists via schema commands, and handle JSON results without reading config files. Co-authored-by: Cursor --- querygate/SKILL.md | 157 +++++++++++++++++++++++++ querygate/command-reference.md | 198 ++++++++++++++++++++++++++++++++ querygate/policy-and-results.md | 173 ++++++++++++++++++++++++++++ 3 files changed, 528 insertions(+) create mode 100644 querygate/SKILL.md create mode 100644 querygate/command-reference.md create mode 100644 querygate/policy-and-results.md diff --git a/querygate/SKILL.md b/querygate/SKILL.md new file mode 100644 index 0000000..e306d58 --- /dev/null +++ b/querygate/SKILL.md @@ -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 ] [--pretty]` | Effective allowlist for one profile | +| `querygate validate [--database ] --sql "" [--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 `; 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. diff --git a/querygate/command-reference.md b/querygate/command-reference.md new file mode 100644 index 0000000..66dc3ff --- /dev/null +++ b/querygate/command-reference.md @@ -0,0 +1,198 @@ +# QueryGate Command Reference + +Runtime agents use QueryGate through the CLI only. Do not read or edit QueryGate config files. + +## Profile Selection + +- `--database ` / `-d ` selects a profile. +- If omitted, QueryGate uses the sole profile when exactly one exists. +- If multiple profiles exist and `--database` is omitted, QueryGate exits with code `2`. + +Discover profile names with: + +```bash +querygate databases --pretty +``` + +## Subcommands + +### `databases` + +List configured database profiles. + +```bash +querygate databases +querygate databases --pretty +``` + +**Stdout success payload:** + +```json +{ + "ok": true, + "databases": [ + { + "name": "app", + "description": "Safe production app data for support agents.", + "dialect": "postgres" + } + ] +} +``` + +### `schema` + +Print the effective allowlist for one profile. Only allowed tables and columns appear. + +```bash +querygate schema --database app +querygate schema --database app --pretty +``` + +**Stdout success payload:** + +```json +{ + "ok": true, + "database": "app", + "description": "Safe production app data for support agents.", + "dialect": "postgres", + "default_schema": "public", + "tables": { + "public.users": { + "columns": ["id", "created_at", "country"] + }, + "public.orders": { + "columns": "*" + } + }, + "functions": ["count", "sum", "avg", "min", "max", "date_trunc"] +} +``` + +- `"columns": "*"` means `SELECT *` is allowed for that table. +- A list of column names means only those columns may be referenced. + +### `validate` + +Parse and policy-check SQL without executing. + +```bash +querygate validate --database app --sql "select id from public.users limit 10" +querygate validate --database app --sql "select id from public.users limit 10" --pretty +``` + +SQL via stdin: + +```bash +querygate validate --database app < query.sql +``` + +**Stdout success payload:** + +```json +{ + "ok": true, + "database": "app", + "valid": true +} +``` + +### `run` + +Validate and execute SQL in a read-only transaction. + +```bash +querygate run --database app --sql "select id, created_at from public.users limit 10" +querygate run --database app --sql "select id, created_at from public.users limit 10" --pretty +querygate run --database app < query.sql +``` + +**Stdout success payload:** + +```json +{ + "ok": true, + "columns": ["id", "created_at"], + "rows": [ + { "id": 123, "created_at": "2026-05-19T10:30:00Z" } + ], + "row_count": 1, + "truncated": false, + "database": "app" +} +``` + +| Field | Meaning | +| ----- | ------- | +| `columns` | Result column names | +| `rows` | Array of row objects (column name → JSON value) | +| `row_count` | Number of rows returned (after truncation) | +| `truncated` | `true` if an automatic row cap removed extra rows | +| `database` | Profile name used | + +Cell types in `rows`: string, number, boolean, or `null`. Unknown PostgreSQL types may appear as strings. + +### `init` (setup only) + +Generates QueryGate setup from database introspection. Normal data-access agents should **not** run `init` unless the user explicitly requests setup help. Do not inspect or document output paths from `init` for routine querying. + +## Flags + +| Flag | Applies to | Purpose | +| ---- | ---------- | ------- | +| `--database` / `-d` | `schema`, `validate`, `run` | Profile name | +| `--sql` / `-s` | `validate`, `run` | SQL string (stdin if omitted) | +| `--pretty` | all subcommands with JSON output | Pretty-print JSON | + +## Stdin And SQL Input + +- Provide SQL with `--sql "..."` or pipe on stdin. +- QueryGate trims whitespace; empty SQL is an error. +- Only a single statement is accepted. + +## Stdout Vs Stderr + +| Outcome | Stream | Format | +| ------- | ------ | ------ | +| Success | stdout | `{ "ok": true, ... }` | +| Failure | stderr | `{ "ok": false, "error": { ... } }` | + +Always parse JSON from the correct stream. Check the process exit code. + +## Exit Codes + +| Code | Category | +| ---- | -------- | +| 0 | Success | +| 2 | Setup / profile / config loading (agent: do not open config files; ask user/admin) | +| 3 | SQL parse error | +| 4 | Policy validation error | +| 5 | Database connection or execution error | +| 1 | Other internal error | + +## Error JSON Shape + +```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.", + "location": { "line": 1, "column": 1 }, + "details": ["Additional violation messages when multiple policy errors exist"] + } +} +``` + +`hint`, `location`, and `details` may be omitted when empty. + +## Typical Agent Sequence + +```bash +querygate databases --pretty +querygate schema --database app --pretty +querygate validate --database app --sql "select count(*) from public.users" --pretty +querygate run --database app --sql "select count(*) from public.users" --pretty +``` diff --git a/querygate/policy-and-results.md b/querygate/policy-and-results.md new file mode 100644 index 0000000..eda269b --- /dev/null +++ b/querygate/policy-and-results.md @@ -0,0 +1,173 @@ +# QueryGate Policy And Results + +Agents learn what is queryable from `querygate schema` output only. Do not open QueryGate config or policy files. + +## Allowlist Semantics + +QueryGate enforces a deny-by-default allowlist per database profile. + +| Observation from `schema` | Meaning | +| ------------------------- | ------- | +| Table present | Table may appear in `FROM` | +| Table absent | Not queryable through QueryGate | +| `"columns": ["id", "country"]` | Only those columns may be referenced | +| `"columns": "*"` | `SELECT *` is allowed for that table | +| Function in `functions` array | May use in expressions (e.g. `count(*)`) | +| Function absent | Not allowed | + +Denied tables and columns are **omitted** from `schema` output. Omission means unavailable—not permission to use another database tool. + +## Query Constraints + +### Allowed + +- Exactly one PostgreSQL `SELECT` statement +- Tables and columns visible in `querygate schema` +- Functions listed in `querygate schema` +- Subqueries in `FROM` (derived tables) +- `UNION` / `INTERSECT` / `EXCEPT` when each branch is a valid `SELECT` +- Explicit `LIMIT` + +### Not Allowed + +| Construct | Typical `error.code` | +| --------- | -------------------- | +| `INSERT`, `UPDATE`, `DELETE`, DDL | `statement_not_allowed` or `sql_parse_error` | +| Multiple statements | `sql_parse_error` | +| `SELECT INTO` | `statement_not_allowed` | +| `VALUES` alone | `statement_not_allowed` | +| `FOR UPDATE` / `FOR SHARE` | `statement_not_allowed` | +| Disallowed table | `table_not_allowed` | +| Disallowed column | `column_not_allowed` | +| Disallowed function | `function_not_allowed` | +| Unqualified column in multi-table query | `ambiguous_column` | + +### `SELECT *` Rules + +Use `SELECT *` only when `querygate schema` shows `"columns": "*"` for that table. Otherwise list columns explicitly. + +### Identifier Normalization + +QueryGate normalizes identifiers before policy lookup: + +- Lowercase +- Double quotes stripped + +Write SQL with lowercase `schema.table` and column names when practical. + +### Joins + +Qualify columns with table aliases in multi-table queries: + +```sql +select u.id, o.id +from public.users u +join public.orders o on o.user_id = u.id +limit 100 +``` + +## Execution Safeguards + +When `querygate run` executes: + +1. **Read-only transaction** — `BEGIN READ ONLY`, rolled back after the query. +2. **Statement timeout** — per-profile limit applied via `SET LOCAL statement_timeout`. +3. **Row cap** — if the query has no top-level `LIMIT`, QueryGate wraps it and returns at most the profile row cap. Check `truncated` in the response. + +If `truncated` is `true`: + +- Tell the user results were capped. +- Refine with filters, aggregates, ordering, or a smaller explicit `LIMIT`. +- Do not treat capped rows as a complete dataset for broad conclusions. + +## Interpreting Success Results + +```json +{ + "ok": true, + "columns": ["country", "user_count"], + "rows": [ + { "country": "US", "user_count": 42 }, + { "country": "CA", "user_count": 7 } + ], + "row_count": 2, + "truncated": false, + "database": "app" +} +``` + +| Situation | Interpretation | +| --------- | -------------- | +| `row_count > 0` | Use `rows` to answer; cite `database` profile when relevant | +| `row_count == 0` | Query succeeded; no matching data | +| `truncated == true` | Partial result set; narrow and re-run if needed | + +Summarize results in natural language unless the user asks for raw JSON. + +## Error Remediation + +### Exit 2 — Setup / profile + +Examples: profile not found, multiple profiles without `--database`, QueryGate not set up. + +**Do:** + +1. Run `querygate databases --pretty` if it works. +2. Confirm the correct `--database` name. +3. Tell the user or admin that QueryGate setup needs attention. + +**Do not:** Open, read, or ask the user to paste QueryGate config files. + +### Exit 3 — Parse + +Examples: invalid SQL, multiple statements, non-`SELECT` statement. + +**Do:** Rewrite as one valid PostgreSQL `SELECT`. + +### Exit 4 — Policy + +Examples: disallowed table, column, or function. + +**Do:** + +1. Run `querygate schema --database --pretty`. +2. Rewrite the query using only listed tables, columns, and functions. +3. If the user needs data outside the allowlist, ask them to request access changes from an admin—do not bypass QueryGate. + +**Do not:** Retry with `psql`, ORMs, or direct connections. + +| `error.code` | Fix | +| ------------ | --- | +| `table_not_allowed` | Use a table from `schema` or request access | +| `column_not_allowed` | Remove or replace column; check `schema` | +| `function_not_allowed` | Use a listed function or rewrite without it | +| `statement_not_allowed` | Simplify to a single plain `SELECT` | +| `ambiguous_column` | Add table alias prefix (`u.id`) | +| `policy_validation_error` | Read `details`; fix all listed violations | + +### Exit 5 — Execution + +Examples: connection failure, missing runtime credentials, timeout, query error at execution time. + +**Do:** + +- Read `error.message` and `error.hint` from stderr JSON. +- If the hint mentions an environment variable name, report that setup may be incomplete—do not ask for or print the variable's value. +- For timeouts, add filters, reduce selected columns, or use a smaller `LIMIT`. + +**Do not:** Inspect QueryGate config files to find connection details. + +### Exit 1 — Internal + +Report `error.message` and `details` if present. Escalate to user/admin if persistent. + +## Answering The User + +When reporting results or failures: + +- Name the database profile (`database` field) when multiple profiles exist. +- Note truncation when `truncated` is `true`. +- Frame policy denials as access boundaries, not missing data in the database. +- Frame setup failures as QueryGate configuration issues requiring admin help—not as invitations to bypass the gateway. + +When the user needs columns or tables not in `schema` output, say they are outside the current QueryGate allowlist and an admin must extend access or provide a safe view. From bb7d06f09b994086a240859db8089a7219ca48c9 Mon Sep 17 00:00:00 2001 From: orgoldfus Date: Sun, 24 May 2026 12:32:24 +0300 Subject: [PATCH 2/3] Add an AGENTS.md file for coding agents --- AGENTS.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..220c5cf --- /dev/null +++ b/AGENTS.md @@ -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 ` | +| 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 -- ` | + +## 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. From 03890a985d15eee518740c292faf0322e44c3b5c Mon Sep 17 00:00:00 2001 From: orgoldfus Date: Sun, 24 May 2026 12:35:52 +0300 Subject: [PATCH 3/3] rename skills folder --- {querygate => skills}/SKILL.md | 0 {querygate => skills}/command-reference.md | 0 {querygate => skills}/policy-and-results.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {querygate => skills}/SKILL.md (100%) rename {querygate => skills}/command-reference.md (100%) rename {querygate => skills}/policy-and-results.md (100%) diff --git a/querygate/SKILL.md b/skills/SKILL.md similarity index 100% rename from querygate/SKILL.md rename to skills/SKILL.md diff --git a/querygate/command-reference.md b/skills/command-reference.md similarity index 100% rename from querygate/command-reference.md rename to skills/command-reference.md diff --git a/querygate/policy-and-results.md b/skills/policy-and-results.md similarity index 100% rename from querygate/policy-and-results.md rename to skills/policy-and-results.md