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
23 changes: 16 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]

services:
postgres:
image: postgres:16
Expand Down Expand Up @@ -38,17 +43,19 @@ jobs:
--health-timeout 5s
--health-retries 15

env:
POSTGRES_URL: postgresql://gate:gate@localhost:5432/writegate
MYSQL_URL: mysql://gate:gate@127.0.0.1:3306/writegate

steps:
- uses: actions/checkout@v4

- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}

- name: Enable live DB integration (3.12 only)
if: matrix.python-version == '3.12'
run: |
echo "POSTGRES_URL=postgresql://gate:gate@localhost:5432/writegate" >> "$GITHUB_ENV"
echo "MYSQL_URL=mysql://gate:gate@127.0.0.1:3306/writegate" >> "$GITHUB_ENV"

- name: Install (editable + extras)
run: |
Expand All @@ -64,14 +71,16 @@ jobs:
run: make test

- name: Build wheel
if: matrix.python-version == '3.12'
run: python -m build

- name: Install wheel into clean venv and smoke
if: matrix.python-version == '3.12'
run: |
python -m venv /tmp/wheel-venv
/tmp/wheel-venv/bin/pip install -U pip
/tmp/wheel-venv/bin/pip install dist/*.whl
/tmp/wheel-venv/bin/pip install "pytest>=8" pyyaml "duckdb>=1.1" "sqlglot>=25" "psycopg[binary]>=3.1" pymysql
/tmp/wheel-venv/bin/pip install "pytest>=8" pyyaml "duckdb>=1.1" "sqlglot>=25,<31" "psycopg[binary]>=3.1" pymysql
/tmp/wheel-venv/bin/python -c "import write_gate; print('installed', write_gate.__version__)"
# Installed wheel has no checkout seed; init cwd policy/catalog. Clear service URLs so smoke uses DuckDB.
unset POSTGRES_URL MYSQL_URL DATABASE_URL || true
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ seed/orders.csv
.logs/demo_audit.jsonl
build/
dist/

.pytest_tmp/
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to **sql-write-gate** are documented here.

## [1.1.3] — 2026-09-09

### Security (P0 HTTP trust boundary)
- `serve` locks database / policy / catalog / environment at startup; request bodies may only supply `sql`, `actor`, `model_id`, `prompt_summary`.
- Body fields that override `policy` / `catalog` / `database` / `db_path` / `environment` are rejected (`400 trust_boundary_violation`).
- Non-loopback binds (including `0.0.0.0` / `::`) require `--auth-token` or `SQL_WRITE_GATE_HTTP_TOKEN`; all-interfaces without auth is refused at startup.
- Audit SQL defaults to literal **redact** (`SQL_WRITE_GATE_AUDIT_SQL_MODE=redact|hash|plain`).
- `sqlglot` compatibility ceiling: `>=25,<31`.
- CI matrix covers Python 3.11 and 3.12 (live Postgres/MySQL integration 3.12-only).

## [1.1.2] — 2026-09-07

### Docs
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ sql-write-gate check "DELETE FROM orders"
- **非生产唯一边界 / 非唯一边界** — 须与最小权限 DB 角色、网络隔离、人工流程并用
- 仅声明矩阵:DuckDB / PostgreSQL / MySQL / SQLite + 已列 SQL;未列语法拒绝
- 不是分布式审批锁、MySQL wire 代理、Web UI、企业 DQ/血缘/多租户平台
- HTTP `serve` 请求体仍可覆盖 policy/catalog/database(尚未 server-lock);本地固定 flag 的 CLI/MCP 更稳妥
- HTTP `serve`policy/catalog/database 已在启动时 server-lock;非 loopback / `0.0.0.0` 须 `--auth-token` / `SQL_WRITE_GATE_HTTP_TOKEN`(详见下方 Trust boundary)
- GitHub Latest Release 可能滞后 `main`;以包版本 / commit 为准

---
Expand Down Expand Up @@ -148,7 +148,9 @@ sql-write-gate init # scaffold policy.yaml + catalog.json
DataPilot calls this gate **outbound**. Prefer MCP `query_sql` / `write_sql`, CLI `check` / `exec` / `proxy`, or HTTP:

```bash
sql-write-gate serve --host 127.0.0.1 --port 8787
sql-write-gate serve --host 127.0.0.1 --port 8787 \
--policy policy.yaml --catalog catalog.json --database seed/warehouse.duckdb
# Non-loopback / 0.0.0.0 requires: --auth-token SECRET (or SQL_WRITE_GATE_HTTP_TOKEN)
```

| Method | Path | Behavior |
Expand All @@ -159,14 +161,15 @@ sql-write-gate serve --host 127.0.0.1 --port 8787
| `POST` | `/v1/block` | Alias of `/v1/check` |
| `POST` | `/v1/datapilot` | Alias of `/v1/execute` (1.1 semantics unchanged) |

Request JSON: `{ "sql": "...", "actor"?, "model_id"?, "prompt_summary"?, "database"?, "db_path"?, "catalog"?, "policy"? }`.
Request JSON: `{ "sql": "...", "actor"?, "model_id"?, "prompt_summary"? }` only. `serve` locks `--policy` / `--catalog` / `--database` / environment at startup; body overrides of those fields are **rejected**.

Response always includes `action` (`ALLOW` \| `BLOCK` \| `REQUIRE_APPROVAL`), `rule_id`, `reason`, `risk_score`, `risk_factors`, `executed`. Treat anything other than `ALLOW` as non-executing.

### Honest boundaries (current — docs only)
### Trust boundary (HTTP `serve`)

- **HTTP binding is not server-locked yet.** `sql-write-gate serve --policy/--catalog/--database` sets defaults, but each request body may still override `policy` / `catalog` / `database` / `db_path`. Do **not** treat body-supplied paths as a trust boundary in production; next hardening pass will bind these server-side only. CLI/MCP started with fixed flags remain the safer local path today.
- **GitHub Release lags main.** Package / `main` is **1.1.2** (`7dc85dd`); GitHub **Latest Release** is still **v1.0.1**. Prefer install-from-main / pin commit `7dc85dd` for suite acceptance until a v1.1.x Release is cut.
- **Server-locked at startup.** `sql-write-gate serve --policy/--catalog/--database` (plus environment from the locked policy) is bound for the process lifetime. Request bodies may only supply `sql` / `actor` / `model_id` / `prompt_summary`; overrides of `policy` / `catalog` / `database` / `db_path` / `environment` return `400 trust_boundary_violation`.
- **Auth for non-loopback.** Binding `127.0.0.1` / `::1` may omit auth. Non-loopback hosts (including `0.0.0.0` / `::`) **require** `--auth-token` or `SQL_WRITE_GATE_HTTP_TOKEN`; all-interfaces without auth is refused at startup. Present `Authorization: Bearer <token>` or `X-SQLGuard-Token`.
- **GitHub Release lags main.** Package / `main` tracks the latest commit; GitHub **Latest Release** may lag. Prefer install-from-main / pin the tip SHA for suite acceptance until a matching Release is cut.

### GameStream-style permissions

Expand Down Expand Up @@ -222,7 +225,7 @@ Anything **not** in this matrix (other warehouses, wire-protocol proxies, distri
- Approval state machine (SQLite source of truth + JSONL mirror): `pending`→`executing`→`succeeded`|`failed`|`unknown` (+ `rejected`)
- Atomic claim under `fcntl.flock` + SQLite `BEGIN IMMEDIATE` (single-host; fail closed without flock)
- Three-state execute outcomes; **`unknown`/`executing` never auto-retried** — use `resolve` or `approve --allow-unknown-retry` after manual DB verify
- JSONL audit (redacts URL passwords; records execute failures / unknown; `request_id` + `approval_id` + `execution_outcome` correlation; rotatable)
- JSONL audit (redacts URL passwords; **SQL literals redacted by default** via `SQL_WRITE_GATE_AUDIT_SQL_MODE=redact|hash|plain`; records execute failures / unknown; `request_id` + `approval_id` + `execution_outcome` correlation; rotatable)

## Platform support matrix

Expand Down Expand Up @@ -385,6 +388,8 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.
| `SQL_WRITE_GATE_RESULT_ROW_LIMIT` | `1000` | Cap SELECT/approve rows (truncate + `truncated=true`) |
| `SQL_WRITE_GATE_RESULT_BYTE_LIMIT` | `0` (off) | Hard byte cap on materialized rows (payload ≤ limit, or `ResultOversizeError` when `RESULT_OVERSIZE=block`; oversized single row never returned intact) |
| `SQL_WRITE_GATE_RESULT_OVERSIZE` | `truncate` | `truncate` (shrink/omit to keep ≤ byte/row caps) or `block` (`ResultOversizeError`) |
| `SQL_WRITE_GATE_AUDIT_SQL_MODE` | `redact` | Audit SQL storage: `redact` (literal scrub, default), `hash` (`sha256:…`), or `plain` (verbatim) |
| `SQL_WRITE_GATE_HTTP_TOKEN` | (optional on loopback) | Bearer token for `serve`; **required** for non-loopback / `0.0.0.0` binds |
| `SQL_WRITE_GATE_AUDIT_MAX_BYTES` | `10 MiB` | Rotate audit / approvals JSONL by size |
| `SQL_WRITE_GATE_AUDIT_ROTATE_DAILY` | `false` | Also rotate JSONL per UTC day |
| `SQL_WRITE_GATE_REQUEST_ID` | auto uuid4 | Audit correlation id |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "sql-write-gate"
version = "1.1.2"
version = "1.1.3"
description = "SQLGuard — policy firewall for AI agents writing to databases (sql-write-gate)"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.11"
dependencies = [
"duckdb>=1.1.0",
"sqlglot>=25.0.0",
"sqlglot>=25.0.0,<31",
"pyyaml>=6.0",
]

Expand Down
2 changes: 1 addition & 1 deletion src/write_gate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
from write_gate.wrapper import WriteGate

__all__ = ["WriteGate", "Evidence", "Decision", "__version__"]
__version__ = "1.1.2"
__version__ = "1.1.3"
Loading
Loading