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
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ DATABASE_URL=postgresql://user:pass@host:5432/queryshield

# --- Required: vault encryption key (Fernet) ---
# Generate once with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# If this changes, all stored connection strings become unreadable.
# Encrypts database connection strings AND named credentials (the vault feature).
# There is one key. Losing it makes every stored secret unreadable.
# Connection-string and named-credential routes fail closed when this is unset.
# Session cookies use SESSION_KEY, or VAULT_KEY when SESSION_KEY is unset.
# There is no built-in fallback. Do not commit a key.
VAULT_KEY=

# --- Optional: cache ---
Expand All @@ -17,6 +21,9 @@ REDIS_URL=

# --- Billing (Stripe) ---
# Paste from the Stripe dashboard. Checkout and webhooks fail closed when unset.
# One Stripe customer per workspace. Credential storage is included in the
# QueryShield prices below — do not create separate AgentVault products.
# InjectShield (the free funnel) keeps its own signup until you fold it.
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_STARTER=
Expand Down
25 changes: 16 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# QueryShield — Claude Code Context

## What this is
Secure SQL proxy between AI agents and customer databases. Agents POST to
`/v1/query` in NL or structured SQL. QueryShield validates → translates →
applies row-level security → executes → audits. Agents never see connection
strings.
Lead product of one agent security suite. Agents POST to `/v1/query` in NL
or structured SQL. QueryShield validates → translates → applies row-level
security → executes → audits. The same `qs_` API key and the same Stripe
customer also cover the named credential vault (`credentials.py`), ported
from AgentVault. InjectShield (https://github.com/bch1212/injectshield,
https://injectshield.dev) is the free prompt-injection funnel and is not
vendored here. Agents never see database connection strings.

## Architecture
- `proxy.py` — engine. Every query flows through `execute_query()`.
- `safety.py` — sqlglot AST validator. **Never use keyword filtering.**
- `rls.py` — AST-level WHERE injection + schema/table whitelists. Never
string concat.
- `vault.py` — Fernet-encrypted credential store, in our internal Postgres.
Replaces AWS Secrets Manager from the original spec (Brett's stack uses
Railway, no AWS).
- `vault.py` — Fernet-encrypted database connection strings, in our internal
Postgres. Replaces AWS Secrets Manager from the original spec (Brett's
stack uses Railway, no AWS). Never returned by any API.
- `credential_crypto.py` / `credentials.py` / `credential_api.py` — named
secret vault (AgentVault folded in). Same `VAULT_KEY`, same tenant, same
`qs_` key. Empty permission patterns fail closed. Audit metadata only.
- `nl_to_sql.py` — Claude tool_use loop with `get_schema` + `validate_query`
tools. Prompt caching on system + schema.
- `schema.py` — DB introspection with two-tier cache (in-process + Redis).
Expand All @@ -36,9 +42,10 @@ strings.
in depth — even if safety + RLS were bypassed, the DB would refuse writes.

## Pricing tiers (`models.TIER_LIMITS`)
- Starter: $500/mo · 3 DBs · 1M queries/mo
- Pro: $1,500/mo · 10 DBs · 10M queries/mo · audit export
- Starter: $500/mo · 3 DBs · 25 credentials · 1M queries/mo
- Pro: $1,500/mo · 10 DBs · 100 credentials · 10M queries/mo · audit export
- Enterprise: $3,500/mo · unlimited · SSO, SIEM webhook
- InjectShield is the free funnel, not a QueryShield Stripe price.

## MRR target
$32.5K @ 15 customers (10 Pro + 5 Enterprise). First 3 customers =
Expand Down
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![tests](https://github.com/bch1212/queryshield/actions/workflows/test.yml/badge.svg)](https://github.com/bch1212/queryshield/actions/workflows/test.yml)

Secure SQL proxy between AI agents and enterprise databases.
Agent security suite. QueryShield is the lead product: a secure SQL proxy and a named credential vault, one `qs_` API key, one Stripe customer, and this docs site.

Agents call a single endpoint in plain English (or structured SQL). QueryShield:

Expand All @@ -17,6 +17,10 @@ Agents call a single endpoint in plain English (or structured SQL). QueryShield:

Agents never see connection strings.

The credential vault stores other secrets (API tokens the agent is allowed to use). It is the AgentVault product, folded into this service. Permission patterns decide which names an agent can read. Database connection strings stay in a separate encrypted store and are not readable through the vault.

**Free tier / funnel:** [InjectShield](https://injectshield.dev) scans untrusted text for prompt injection. It stays in its own repository ([bch1212/injectshield](https://github.com/bch1212/injectshield)) and its own API ([api.injectshield.dev](https://api.injectshield.dev)). Start there, then create one QueryShield workspace when an agent needs a database or a vault. Docs: [injectshield.dev/docs](https://injectshield.dev/docs). Suite page on this service: `/suite`.

---

## Quickstart
Expand Down Expand Up @@ -75,6 +79,31 @@ curl -X POST localhost:8000/v1/policies \
"row_filters": { "users": "tenant_id = 42" }
}'

# 4b) Store a named credential and let one agent read it.
# The value is write-only on this call. Empty permissions grant nothing.
curl -X POST localhost:8000/v1/credentials \
-H 'X-Admin-Key: qs_...' \
-H 'Content-Type: application/json' \
-d '{
"name": "openai_key",
"provider": "openai",
"value": "YOUR_SECRET"
}'

curl -X POST localhost:8000/v1/agents \
-H 'X-Admin-Key: qs_...' \
-H 'Content-Type: application/json' \
-d '{
"name": "writer",
"tenant_id": "<tenant>",
"credential_permissions": ["openai_*"],
"budget_daily": 5
}'

# The agent uses the same qs_ key it uses for /v1/query.
curl -X POST localhost:8000/v1/vault/get/openai_key \
-H 'X-API-Key: qs_...'

# 5) The agent queries.
curl -X POST localhost:8000/v1/query \
-H 'X-API-Key: qs_...' \
Expand Down Expand Up @@ -135,8 +164,12 @@ Tools exposed:

- `query_database(database_alias, question, max_rows)` — natural-language
- `query_database_sql(database_alias, sql, max_rows)` — pre-built SELECT
- `list_credentials()` — names this agent may read
- `get_credential(name, cost)` — one permitted secret, same API key
- `get_audit_log(limit)` — recent attempts for the calling agent

The PyPI package `queryshield-mcp` picks up the vault tools on its next release. Until then, `python -m queryshield.mcp_server` from this repo already exposes them.

---

## Security model
Expand All @@ -149,6 +182,8 @@ Tools exposed:
| Agent reads tables outside its scope | RLS schema + table whitelist |
| Agent reads other tenants' rows | `row_filters` injected via AST `.where()` |
| Connection string leaks via stack traces | Fernet-encrypted, never returned in any API |
| Agent reads a secret it was not granted | fnmatch permissions; empty list fails closed |
| Vault audit becomes the secret | credential access log stores metadata only |
| Audit log becomes the data exfil vector | only metadata is stored — never rows |
| `VAULT_KEY` rotation | re-encrypt rows under new key (script-driven) |

Expand All @@ -159,13 +194,14 @@ that lands there should ship with a test in `tests/test_safety.py`.

## Pricing

| Tier | Monthly | Databases | Queries / month | Notes |
| ----------- | ------- | --------- | --------------- | ---------------------- |
| Starter | $500 | 3 | 1,000,000 | |
| Pro | $1,500 | 10 | 10,000,000 | audit export |
| Enterprise | $3,500 | unlimited | unlimited | SSO, SIEM webhook |
| Tier | Monthly | Databases | Credentials | Queries / month | Notes |
| ------------ | ------- | --------- | ----------- | --------------- | ---------------------- |
| InjectShield | free | — | — | — | prompt-injection funnel, separate site |
| Starter | $500 | 3 | 25 | 1,000,000 | |
| Pro | $1,500 | 10 | 100 | 10,000,000 | audit export |
| Enterprise | $3,500 | unlimited | unlimited | unlimited | SSO, SIEM webhook |

Targets `$32.5K MRR @ 15 customers (10 Pro + 5 Enterprise)`.
QueryShield tiers are one Stripe customer. InjectShield sign-up stays on [injectshield.dev](https://injectshield.dev) until that billing is folded in. Targets `$32.5K MRR @ 15 customers (10 Pro + 5 Enterprise)`.

---

Expand All @@ -192,7 +228,7 @@ pip install pytest
python -m pytest tests/
```

42 tests cover:
102 tests cover the original safety, RLS, proxy, and HTTP suites plus the credential vault (encryption, `qs_` key hashing, permission checks, budgets, tenant isolation, and the column migration for existing databases) and fail-closed `VAULT_KEY` / `SESSION_KEY` checks. The original cases:

- AST safety (24 cases — direct DDL, comments, encoded keywords, multiple
statements, forbidden functions, missing LIMIT)
Expand Down
6 changes: 5 additions & 1 deletion packages/queryshield-mcp/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# queryshield-mcp

MCP client for [QueryShield](https://queryshield.dev) — a secure SQL proxy that sits between AI agents and customer databases. Agents call a single endpoint; QueryShield translates NL → SQL via Claude, validates at the AST level (no DDL/DML), enforces per-agent row-level security, and audit-logs every query.
MCP client for [QueryShield](https://queryshield.dev) — the agent security suite. One `QUERYSHIELD_API_KEY` reaches the SQL proxy and the named credential vault. [InjectShield](https://injectshield.dev) is the free prompt-injection funnel and stays a separate project.

## Install

Expand Down Expand Up @@ -29,8 +29,12 @@ Self-hosting? Set `QUERYSHIELD_BASE_URL` to point at your deploy.

- `query_database(database_alias, question, max_rows)` — natural-language query
- `query_database_sql(database_alias, sql, max_rows)` — structured SELECT
- `list_credentials()` — credential names this agent may read
- `get_credential(name, cost=0)` — decrypt one permitted credential
- `get_audit_log(limit)` — recent attempts for the calling agent

`list_credentials` and `get_credential` are in this source tree. They ship in the next PyPI release of `queryshield-mcp`. The Docker image used for MCP directory introspection still installs the published package until that release is out.

## License

MIT
Expand Down
27 changes: 27 additions & 0 deletions packages/queryshield-mcp/queryshield_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ async def query_database_sql(
return r.json()


@mcp.tool()
async def list_credentials() -> dict[str, Any]:
"""List credential names this agent is allowed to read. Names only."""
async with httpx.AsyncClient(timeout=30) as client:
r = await client.get(
f"{_base_url()}/v1/vault/credentials",
headers={"X-API-Key": _api_key()},
)
if r.status_code >= 400:
return {"error": r.text, "status": r.status_code}
return r.json()


@mcp.tool()
async def get_credential(name: str, cost: float = 0.0) -> dict[str, Any]:
"""Read one named credential. Uses the same QUERYSHIELD_API_KEY as queries."""
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
f"{_base_url()}/v1/vault/get/{name}",
headers={"X-API-Key": _api_key()},
params={"cost": cost},
)
if r.status_code >= 400:
return {"error": r.text, "status": r.status_code}
return r.json()


@mcp.tool()
async def get_audit_log(limit: int = 50) -> dict[str, Any]:
"""Recent audit entries for the calling agent."""
Expand Down
Loading
Loading