sql-write-gate 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. sql_write_gate-0.16.0/LICENSE +21 -0
  2. sql_write_gate-0.16.0/PKG-INFO +482 -0
  3. sql_write_gate-0.16.0/README.md +461 -0
  4. sql_write_gate-0.16.0/pyproject.toml +36 -0
  5. sql_write_gate-0.16.0/setup.cfg +4 -0
  6. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/PKG-INFO +482 -0
  7. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/SOURCES.txt +63 -0
  8. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/dependency_links.txt +1 -0
  9. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/entry_points.txt +2 -0
  10. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/requires.txt +15 -0
  11. sql_write_gate-0.16.0/src/sql_write_gate.egg-info/top_level.txt +1 -0
  12. sql_write_gate-0.16.0/src/write_gate/__init__.py +7 -0
  13. sql_write_gate-0.16.0/src/write_gate/__main__.py +6 -0
  14. sql_write_gate-0.16.0/src/write_gate/adapters/__init__.py +33 -0
  15. sql_write_gate-0.16.0/src/write_gate/adapters/base.py +101 -0
  16. sql_write_gate-0.16.0/src/write_gate/adapters/duckdb.py +41 -0
  17. sql_write_gate-0.16.0/src/write_gate/adapters/mysql.py +115 -0
  18. sql_write_gate-0.16.0/src/write_gate/adapters/postgres.py +96 -0
  19. sql_write_gate-0.16.0/src/write_gate/adapters/sqlite.py +116 -0
  20. sql_write_gate-0.16.0/src/write_gate/approvals.py +205 -0
  21. sql_write_gate-0.16.0/src/write_gate/audit.py +120 -0
  22. sql_write_gate-0.16.0/src/write_gate/cases.py +30 -0
  23. sql_write_gate-0.16.0/src/write_gate/catalog.py +79 -0
  24. sql_write_gate-0.16.0/src/write_gate/cli.py +397 -0
  25. sql_write_gate-0.16.0/src/write_gate/config.py +117 -0
  26. sql_write_gate-0.16.0/src/write_gate/db.py +5 -0
  27. sql_write_gate-0.16.0/src/write_gate/decision.py +143 -0
  28. sql_write_gate-0.16.0/src/write_gate/engine.py +150 -0
  29. sql_write_gate-0.16.0/src/write_gate/guards/__init__.py +17 -0
  30. sql_write_gate-0.16.0/src/write_gate/guards/blast_radius.py +73 -0
  31. sql_write_gate-0.16.0/src/write_gate/guards/destructive.py +85 -0
  32. sql_write_gate-0.16.0/src/write_gate/guards/environment.py +41 -0
  33. sql_write_gate-0.16.0/src/write_gate/guards/freshness.py +102 -0
  34. sql_write_gate-0.16.0/src/write_gate/guards/pii.py +84 -0
  35. sql_write_gate-0.16.0/src/write_gate/guards/schema.py +147 -0
  36. sql_write_gate-0.16.0/src/write_gate/hooks.py +360 -0
  37. sql_write_gate-0.16.0/src/write_gate/init.py +84 -0
  38. sql_write_gate-0.16.0/src/write_gate/mcp_server.py +71 -0
  39. sql_write_gate-0.16.0/src/write_gate/mcp_tools.py +197 -0
  40. sql_write_gate-0.16.0/src/write_gate/parser.py +367 -0
  41. sql_write_gate-0.16.0/src/write_gate/paths.py +20 -0
  42. sql_write_gate-0.16.0/src/write_gate/policy.py +49 -0
  43. sql_write_gate-0.16.0/src/write_gate/proxy.py +273 -0
  44. sql_write_gate-0.16.0/src/write_gate/templates/GETTING_STARTED.md +20 -0
  45. sql_write_gate-0.16.0/src/write_gate/templates/catalog.json +32 -0
  46. sql_write_gate-0.16.0/src/write_gate/templates/policy.yaml +12 -0
  47. sql_write_gate-0.16.0/src/write_gate/wrapper.py +207 -0
  48. sql_write_gate-0.16.0/tests/test_adapters.py +318 -0
  49. sql_write_gate-0.16.0/tests/test_approvals.py +266 -0
  50. sql_write_gate-0.16.0/tests/test_audit.py +121 -0
  51. sql_write_gate-0.16.0/tests/test_blast_radius.py +96 -0
  52. sql_write_gate-0.16.0/tests/test_demo_cases.py +86 -0
  53. sql_write_gate-0.16.0/tests/test_destructive.py +56 -0
  54. sql_write_gate-0.16.0/tests/test_environment.py +38 -0
  55. sql_write_gate-0.16.0/tests/test_hooks.py +205 -0
  56. sql_write_gate-0.16.0/tests/test_init.py +75 -0
  57. sql_write_gate-0.16.0/tests/test_mcp.py +108 -0
  58. sql_write_gate-0.16.0/tests/test_mcp_execute.py +206 -0
  59. sql_write_gate-0.16.0/tests/test_mysql.py +252 -0
  60. sql_write_gate-0.16.0/tests/test_mysql_live.py +42 -0
  61. sql_write_gate-0.16.0/tests/test_policy.py +126 -0
  62. sql_write_gate-0.16.0/tests/test_postgres_live.py +51 -0
  63. sql_write_gate-0.16.0/tests/test_proxy.py +289 -0
  64. sql_write_gate-0.16.0/tests/test_readme.py +14 -0
  65. sql_write_gate-0.16.0/tests/test_sqlite.py +244 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sql-write-gate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,482 @@
1
+ Metadata-Version: 2.4
2
+ Name: sql-write-gate
3
+ Version: 0.16.0
4
+ Summary: Policy firewall for AI agents writing to databases (sql-write-gate)
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: duckdb>=1.1.0
10
+ Requires-Dist: sqlglot>=25.0.0
11
+ Requires-Dist: pyyaml>=6.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Provides-Extra: postgres
15
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
16
+ Provides-Extra: mysql
17
+ Requires-Dist: pymysql>=1.1; extra == "mysql"
18
+ Provides-Extra: mcp
19
+ Requires-Dist: mcp<2,>=1.28; extra == "mcp"
20
+ Dynamic: license-file
21
+
22
+ # sql-write-gate
23
+
24
+ [![CI](https://github.com/tangyf07/sql-write-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/tangyf07/sql-write-gate/actions/workflows/ci.yml)
25
+ [![Release](https://img.shields.io/github/v/release/tangyf07/sql-write-gate)](https://github.com/tangyf07/sql-write-gate/releases/latest)
26
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
28
+
29
+
30
+ 写库前门禁 · Policy firewall for AI agents writing to databases.
31
+
32
+ Prevent Claude Code, Codex, Cursor and MCP agents from executing unsafe database operations.
33
+
34
+ ```
35
+ Agent SQL ──► sql-write-gate ──► ALLOW / BLOCK / APPROVAL ──► Database
36
+ ```
37
+
38
+ Deterministic policy engine (sqlglot AST + catalog + policy.yaml). **No LLM. No API key.**
39
+
40
+ ```bash
41
+ pip install sql-write-gate
42
+ sql-write-gate check "DELETE FROM users"
43
+ ```
44
+
45
+ From a clone (editable / extras):
46
+
47
+ ```bash
48
+ pip install -e ".[dev]" # or: make install
49
+ pip install -e ".[mysql]" # optional: pymysql
50
+ pip install 'sql-write-gate[postgres]' # from PyPI: extras use dist name
51
+ ```
52
+
53
+ ```
54
+ BLOCKED
55
+ Risk: critical
56
+ Operation: DELETE
57
+ Table: users
58
+ Rule: delete_without_where
59
+ Reason: DELETE without a WHERE clause is forbidden (full-table delete on users)
60
+ ```
61
+
62
+ ```bash
63
+ sql-write-gate check "DELETE FROM orders"
64
+ # → BLOCKED rule=delete_without_where (no API key)
65
+ ```
66
+
67
+ ## Commands
68
+
69
+ ```bash
70
+ sql-write-gate check "SQL" # evaluate SQL; no execute
71
+ sql-write-gate hook # PreToolUse: block raw psql
72
+ sql-write-gate mcp # MCP stdio (query_sql / write_sql)
73
+ sql-write-gate proxy --sql "..." # gate then execute if ALLOW
74
+ sql-write-gate approve <id> # human approve then write
75
+ sql-write-gate audit # TIME / SOURCE / OP / TABLE / VERDICT
76
+ ```
77
+
78
+ ## What it blocks
79
+
80
+ - [x] `DROP TABLE` / `TRUNCATE` / `ALTER TABLE`
81
+ - [x] `DELETE` / `UPDATE` without `WHERE`
82
+ - [x] Blast-radius: estimated rows over `update_rows` / `delete_rows`
83
+ - [x] Schema: unknown table/column, type mismatch
84
+ - [x] PII writes blocked; `SELECT` of PII columns requires approval
85
+ - [x] Environment policy: per-operation allow / block / approval
86
+ - [x] Freshness: expired partitions (`dt` before cutoff)
87
+ - [x] JSONL audit log (`.logs/audit.jsonl`); `sql-write-gate audit` prints TIME / SOURCE / OP / TABLE / VERDICT
88
+ - [x] Approval queue (`.logs/approvals.jsonl`): `REQUIRE_APPROVAL` is recorded, not executed, until `approve <id>`
89
+ - [x] Deterministic rules — no LLM, no network
90
+
91
+ ## 30-second path
92
+
93
+ ```bash
94
+ pip install sql-write-gate # PyPI
95
+ # from clone: cd sql-write-gate && pip install -e . # or: make install
96
+ sql-write-gate check "DELETE FROM orders"
97
+ # BLOCKED / delete_without_where — no API key required
98
+
99
+ sql-write-gate audit
100
+ # TIME SOURCE OP TABLE VERDICT RULE
101
+ # 2026-09-03 00:40 cli delete orders BLOCK delete_without_where
102
+
103
+ make demo # three write cases + check/hook/mcp/proxy/approve/audit
104
+ make test # pytest -q
105
+ ```
106
+
107
+ `make demo` covers the walkthrough (check / hook / mcp / proxy / approve / audit) after the three DuckDB write cases.
108
+
109
+ `python -m write_gate check "DELETE FROM orders"` works the same.
110
+
111
+ ## v0.2 — PostgreSQL adapter
112
+
113
+ DuckDB is still the default. Pass a URL to use Postgres; all v0.1 guards apply on both adapters.
114
+
115
+ ```python
116
+ from write_gate import WriteGate
117
+
118
+ gate = WriteGate(database="postgresql://user:pass@localhost:5432/app")
119
+ decision, result = gate.execute("DELETE FROM orders")
120
+ # BLOCKED / delete_without_where (AST path; no live `orders` table required)
121
+ ```
122
+
123
+ ```bash
124
+ sql-write-gate check --database "$DATABASE_URL" "DELETE FROM orders"
125
+ # or: export DATABASE_URL=postgresql://...
126
+ ```
127
+
128
+ `postgres://` and `postgresql://` select Postgres; anything else is a DuckDB file path. `WriteGate(database=...)` / `database_url=` / env `DATABASE_URL` are equivalent.
129
+
130
+ Blast-radius on Postgres uses `SELECT COUNT(*) ... WHERE <predicate>` (same `update_rows` / `delete_rows` limits as DuckDB). Optional driver: `pip install 'sql-write-gate[postgres]'` (from clone: `pip install -e ".[postgres]"`). Default `pip install -e .` stays DuckDB-only.
131
+
132
+ The 30-second path above is unchanged.
133
+
134
+ ## v0.3 — Claude Code / Codex PreToolUse hook
135
+
136
+ Agents cannot talk to the DB via raw `psql` (also `mysql`, `mysqlsh`, `duckdb`, `sqlite3`). They must go through `sql-write-gate`. The hook never executes SQL; raw `psql` is never the write path.
137
+
138
+ Copy into the project `.claude/settings.json`:
139
+
140
+ ```json
141
+ {
142
+ "hooks": {
143
+ "PreToolUse": [
144
+ {
145
+ "matcher": "Bash",
146
+ "hooks": [
147
+ {
148
+ "type": "command",
149
+ "command": "sql-write-gate hook"
150
+ }
151
+ ]
152
+ }
153
+ ]
154
+ }
155
+ }
156
+ ```
157
+
158
+ Copy into `.codex/hooks.json` (PreToolUse at root — no wrapping `hooks` key):
159
+
160
+ ```json
161
+ {
162
+ "PreToolUse": [
163
+ {
164
+ "matcher": "Bash",
165
+ "hooks": [
166
+ {
167
+ "type": "command",
168
+ "command": "sql-write-gate hook"
169
+ }
170
+ ]
171
+ }
172
+ ]
173
+ }
174
+ ```
175
+
176
+ 30-second no-IDE demo (no LLM, no API key):
177
+
178
+ ```bash
179
+ printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"psql -c DELETE FROM orders"}}' \
180
+ | sql-write-gate hook
181
+ # exit 2 / BLOCKED / delete_without_where
182
+ ```
183
+
184
+ Non-SQL bash (`ls`, `pytest`) is allowed and stays quiet. Interactive `psql` (no `-c`) is blocked: use `sql-write-gate check|exec`. `REQUIRE_APPROVAL` is also refused (exit 2) so agents cannot silently write in production.
185
+
186
+ DuckDB 30-second path (`sql-write-gate check "DELETE FROM orders"` / `make demo`) is unchanged.
187
+
188
+ ## v0.4 — MCP stdio tools
189
+
190
+ Agents call sql-write-gate as MCP tools. Every `query_sql` / `write_sql` goes through `WriteGate.check` (never `execute`). **No LLM. No API key.** Default `pip install -e .` stays without the MCP SDK.
191
+
192
+ ```bash
193
+ pip install 'sql-write-gate[mcp]' # from clone: pip install -e ".[mcp]"
194
+ sql-write-gate mcp
195
+ # optional: sql-write-gate mcp --database "$DATABASE_URL"
196
+ ```
197
+
198
+ If the extra is missing, the CLI prints `pip install -e ".[mcp]"` (editable hint) and exits 1.
199
+
200
+ Wire it (Claude / Codex `mcpServers`); copy [examples/mcp/config.json](examples/mcp/config.json):
201
+
202
+ ```json
203
+ {
204
+ "mcpServers": {
205
+ "sql-write-gate": {
206
+ "command": "sql-write-gate",
207
+ "args": ["mcp"]
208
+ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ 30-second no-IDE demo (no Agent, no LLM, no API key):
214
+
215
+ ```bash
216
+ python -c 'from write_gate.mcp_tools import write_sql, query_sql; print(write_sql("DELETE FROM orders")); print(query_sql("SELECT 1"))'
217
+ # BLOCK / delete_without_where
218
+ # ALLOW / ok
219
+ ```
220
+
221
+ `SELECT order_id FROM orders LIMIT 1` is also ALLOW. Production policy allows SELECT; `DELETE FROM orders` is still BLOCK. v0.3 hook and v0.1/v0.2 `check` paths are unchanged.
222
+
223
+ ## v0.5 — ALLOW writes persist
224
+
225
+ MCP `write_sql` / `query_sql` call `WriteGate.execute`. **ALLOW writes persist; BLOCK and REQUIRE_APPROVAL do not.** Same path if `DATABASE_URL` is `postgres://` / `postgresql://`. **No LLM. No API key.**
226
+
227
+ 30-second no-IDE demo (no Agent, no LLM, no API key):
228
+
229
+ ```bash
230
+ python -c 'from write_gate.cases import LEGAL_WRITE_SQL; from write_gate.mcp_tools import write_sql, query_sql; from write_gate.paths import DEMO_POLICY_PATH; print(write_sql(LEGAL_WRITE_SQL, policy_path=DEMO_POLICY_PATH)); print(query_sql("SELECT order_id FROM orders WHERE order_id = 900001")); print(write_sql("DELETE FROM orders"))'
231
+ # ALLOW insert persists (order_id=900001)
232
+ # SELECT finds the row
233
+ # BLOCK / delete_without_where
234
+ ```
235
+
236
+ `sql-write-gate check "DELETE FROM orders"` is still BLOCK. Hook `psql -c DELETE FROM orders` still exit 2. `query_sql("SELECT 1")` still ALLOW.
237
+
238
+ ## v0.6 — SQL proxy in front of the warehouse
239
+
240
+ Agents talk to `sql-write-gate proxy` instead of the DB. Incoming SQL goes through WriteGate first. **ALLOW then execute. BLOCK and REQUIRE_APPROVAL do not write.** DuckDB is fully testable without a Postgres server. Same path if `DATABASE_URL` is `postgres://` / `postgresql://`. **No LLM. No API key. No PG wire protocol. No Web UI.**
241
+
242
+ 30-second no-IDE demo (DuckDB, no Agent, no LLM, no API key):
243
+
244
+ ```bash
245
+ sql-write-gate proxy --database seed/warehouse.duckdb --sql "DELETE FROM orders"
246
+ # BLOCKED / delete_without_where (rows unchanged)
247
+
248
+ sql-write-gate proxy --database seed/warehouse.duckdb --policy examples/policy.demo.yaml \
249
+ --sql "INSERT INTO orders (order_id, user_id, amount, dt, status) VALUES (900001, 42, 18.50, '2026-09-01', 'paid')"
250
+ # ALLOWED / executed — SELECT finds order_id=900001
251
+ ```
252
+
253
+ Exit codes match `check`: 0 ALLOW, 1 APPROVAL, 2 BLOCK. One-shot `--sql` (and stdin until EOF) so the command does not hang. Optional `--listen 127.0.0.1:0` text protocol: one SQL per connection.
254
+
255
+ `sql-write-gate check "DELETE FROM orders"` is still BLOCK. Hook `psql -c DELETE FROM orders` still exit 2. MCP `write_sql("DELETE FROM orders")` still BLOCK.
256
+
257
+ ## v0.7 — Real approval queue
258
+
259
+ `REQUIRE_APPROVAL` is no longer a soft skip. SQL that needs approval is recorded in `.logs/approvals.jsonl` and **not executed**. `sql-write-gate approve <id>` then writes. Rejected or never approved does not write. Human approve clears the environment `approval` rule only; PII / destructive / schema still **BLOCK**. `check` and the PreToolUse hook stay evaluate-only (no enqueue, no write). **No LLM. No API key. No Slack. No Web UI.**
260
+
261
+ 30-second DuckDB path (production policy, `insert=approval`):
262
+
263
+ ```bash
264
+ sql-write-gate exec --database seed/warehouse.duckdb --policy examples/policy.yaml \
265
+ "INSERT INTO orders (order_id, user_id, amount, dt, status) VALUES (900001, 42, 18.50, '2026-09-01', 'paid')"
266
+ # REQUIRE_APPROVAL approval_id=<id> (no row)
267
+
268
+ sql-write-gate pending
269
+ sql-write-gate approve <id>
270
+ # ALLOWED / executed
271
+
272
+ sql-write-gate exec --database seed/warehouse.duckdb \
273
+ "SELECT order_id FROM orders WHERE order_id = 900001"
274
+ # finds the row
275
+
276
+ sql-write-gate reject <other-id> # marks rejected, does not write
277
+ ```
278
+
279
+ `DELETE FROM orders` is still **BLOCK** (`delete_without_where`), not queued, no write. Hook `psql -c DELETE FROM orders` still exit 2. Proxy `DELETE FROM orders` still BLOCK. `make demo` still three cases (demo policy `insert=allow`, so legal INSERT is ALLOW without approve).
280
+
281
+ ## v0.8 — Human-readable audit
282
+
283
+ `sql-write-gate audit` prints a glanceable table from `.logs/audit.jsonl`. JSONL on disk is unchanged (`decision` stays `ALLOW` / `BLOCK` / `REQUIRE_APPROVAL`). The **VERDICT** column maps `REQUIRE_APPROVAL` → `APPROVAL`. Empty log: `(no audit records)`. **No LLM. No API key. No Web UI. No DB-backed audit store.**
284
+
285
+ 30-second DuckDB path:
286
+
287
+ ```bash
288
+ sql-write-gate check "DELETE FROM orders"
289
+ # BLOCKED / delete_without_where
290
+
291
+ sql-write-gate audit
292
+ # TIME SOURCE OP TABLE VERDICT RULE
293
+ # 2026-09-03 00:40 cli delete orders BLOCK delete_without_where
294
+ ```
295
+
296
+ `--limit` and `--audit-path` still work (default `.logs/audit.jsonl`). TIME is local Asia/Shanghai (`YYYY-MM-DD HH:MM`); SOURCE is the agent (`cli` / `hook` / `mcp` / `proxy` / `test`).
297
+
298
+ `DELETE FROM orders` is still **BLOCK**. Hook `psql -c DELETE FROM orders` still exit 2. Proxy `DELETE FROM orders` still BLOCK. MCP `write_sql("DELETE FROM orders")` still BLOCK. `approve` still writes only after a human id.
299
+
300
+ ## v0.9 — Take-out-ready 0.9.0
301
+
302
+ Version **0.9.0**. First screen lists check / hook / mcp / proxy / approve / audit. `make demo` walks those CLIs after the three DuckDB write cases (legal ALLOW / expired BLOCK / PII BLOCK). MCP demo calls `write_sql` / `query_sql` (does not hang on stdio). Approve runs on an isolated DuckDB copy so the demo warehouse stays intact. **No LLM. No API key. No PyPI publish. Guards unchanged.**
303
+
304
+ ```bash
305
+ make demo
306
+ # three cases, then:
307
+ # check DELETE FROM orders → BLOCK
308
+ # hook --command psql -c DELETE ... → BLOCK exit 2
309
+ # write_sql DELETE / query_sql SELECT 1
310
+ # proxy --sql DELETE FROM orders → BLOCK
311
+ # exec INSERT (production) → pending; approve <id>; SELECT finds the row
312
+ # audit --audit-path ... → TIME SOURCE OP TABLE VERDICT
313
+ ```
314
+
315
+ `sql-write-gate check "DELETE FROM orders"` is still **BLOCK**. Hook still exit 2. Proxy DELETE still BLOCK.
316
+
317
+ ## v0.12 — GitHub Release
318
+
319
+ Tagged **v0.11.0** with [CHANGELOG.md](CHANGELOG.md) and a GitHub Release. No PyPI publish. No product behavior change.
320
+
321
+ ## v0.14 — README badges + v0.13.0 Release
322
+
323
+ README badges (CI / Release / Python / License). Tagged **v0.13.0** with a GitHub Release. No PyPI. No product behavior change.
324
+
325
+ ## v0.16 — PyPI package name `sql-write-gate`
326
+
327
+ Version **0.16.0**. Distribution name on PyPI is **`sql-write-gate`** (was `write-gate` in earlier pyproject drafts). Import package stays `write_gate`; CLI entry stays `sql-write-gate`.
328
+
329
+ ```bash
330
+ pip install sql-write-gate
331
+ pip install 'sql-write-gate[mysql]' # optional extras
332
+ pip install 'sql-write-gate[postgres]'
333
+ pip install 'sql-write-gate[mcp]'
334
+ ```
335
+
336
+ Trusted Publishing: push a `v*` tag to run [`.github/workflows/publish.yml`](.github/workflows/publish.yml) (OIDC → PyPI). No product behavior change vs 0.15.0.
337
+
338
+ ## Earlier — v0.15.0 GitHub Release
339
+
340
+ Tagged **v0.15.0** with a GitHub Release. No PyPI at that tag. No product behavior change.
341
+
342
+ ## v0.15 — `init` starter scaffold
343
+
344
+ Version **0.15.0**. Scaffold a starter project in the current directory (or `--dir`):
345
+
346
+ ```bash
347
+ sql-write-gate init
348
+ sql-write-gate init --dir /path/to/project
349
+ sql-write-gate init --force # overwrite existing starter files
350
+ ```
351
+
352
+ Writes `policy.yaml`, `catalog.json`, and `GETTING_STARTED.md` (shortest usage). Existing files are skipped unless `--force`. See `GETTING_STARTED.md` for `check "DELETE FROM orders"` and optional `--db` / `--database`.
353
+
354
+ ## v0.13 — SQLite adapter
355
+
356
+ Version **0.13.0**. `sqlite:///` and `sqlite+aiosqlite://` (file-path form) select the SQLite adapter (sqlglot dialect `sqlite`, stdlib `sqlite3` — no extra install).
357
+
358
+ ```bash
359
+ sql-write-gate check --database sqlite:////tmp/wg.db "DELETE FROM orders"
360
+ # → BLOCKED rule=delete_without_where (no live tables required)
361
+ ```
362
+
363
+ ```python
364
+ from write_gate import WriteGate
365
+
366
+ gate = WriteGate(database="sqlite:////tmp/x.db")
367
+ gate.check("DELETE FROM orders") # BLOCK delete_without_where
368
+ ```
369
+
370
+ AST guards still fire without a live DB. DuckDB / Postgres / MySQL / hook / MCP / CI paths are unchanged. **No Web UI. No PyPI publish.**
371
+
372
+ ## v0.11 — GitHub Actions CI
373
+
374
+ Push and pull requests to `main` run [`.github/workflows/ci.yml`](.github/workflows/ci.yml): `pip install -e ".[dev]"` then `make test`. No product behavior change.
375
+
376
+ ## v0.10 — MySQL adapter
377
+
378
+ Version **0.10.0**. `mysql://` and `mysql+pymysql://` select the MySQL adapter (sqlglot dialect `mysql`). Default install is still DuckDB-only; add the optional extra for a live driver:
379
+
380
+ ```bash
381
+ pip install 'sql-write-gate[mysql]' # pymysql>=1.1 (from clone: pip install -e ".[mysql]")
382
+ sql-write-gate check --database mysql://user:pass@localhost/db "DELETE FROM orders"
383
+ # → BLOCKED rule=delete_without_where (no live MySQL required)
384
+ ```
385
+
386
+ ```python
387
+ from write_gate import WriteGate
388
+
389
+ gate = WriteGate(database="mysql://user:pass@localhost/db")
390
+ gate.check("DELETE FROM orders") # BLOCK delete_without_where
391
+ ```
392
+
393
+ AST guards (DELETE/UPDATE without WHERE, DROP, PII, …) still fire without a live DB. DuckDB / Postgres / hook / MCP / approve / audit paths are unchanged. Hook still intercepts raw `mysql` / `mysqlsh` CLIs. **No Web UI. No MySQL wire-protocol proxy. No PyPI publish.**
394
+
395
+ ## Policy
396
+
397
+ Default (`policy.yaml` / `examples/policy.yaml`) is **production**:
398
+
399
+ | operation | rule |
400
+ |-----------|------|
401
+ | select | allow |
402
+ | insert | approval |
403
+ | update | approval |
404
+ | delete | block |
405
+ | ddl | block |
406
+
407
+ Limits: `update_rows: 100`, `delete_rows: 50`.
408
+
409
+ `make demo` three INSERT cases pass `--policy examples/policy.demo.yaml` (insert=allow) so a legal write can still show **ALLOW**. CLI / README screenshots use production policy.
410
+
411
+ ```bash
412
+ sql-write-gate check --policy examples/policy.yaml "UPDATE orders SET status='expired' WHERE id=123"
413
+ sql-write-gate check "SELECT id, name FROM users LIMIT 10"
414
+ sql-write-gate audit
415
+ ```
416
+
417
+ ## Decision model
418
+
419
+ `ALLOW` | `BLOCK` | `REQUIRE_APPROVAL` with `risk` `low|medium|critical`, `rule_id`, `reason`, `evidence`.
420
+
421
+ Guards (any **BLOCK** wins, else any **APPROVAL**, else **ALLOW**):
422
+
423
+ `destructive` → `schema` → `pii` → `freshness` → `blast_radius` → `environment`
424
+
425
+ ## 三条用例 (`make demo`)
426
+
427
+ Dates anchored `as_of=2026-09-02`; partitions older than 7 days (`dt < 2026-08-26`) are expired.
428
+
429
+ | # | 场景 | 期望 | `rule_id` |
430
+ |---|------|------|-----------|
431
+ | 1 | 合法写入:新鲜分区 `dt='2026-09-01'`,只写 `order_id,user_id,amount,dt,status` | ALLOWED | `ok` |
432
+ | 2 | 过期分区:`dt='2026-08-01'` | BLOCKED | `expired_partition` |
433
+ | 3 | PII 写入:INSERT 带 `email` | BLOCKED | `pii_column` |
434
+
435
+ `make demo` then walks check / hook / mcp / proxy / approve / audit (approve uses an isolated DuckDB copy).
436
+
437
+ 示例表 `orders` 列:`order_id, user_id, amount, dt, email, phone, status`。种子约 120 行。
438
+
439
+ **唯一写入口**:`WriteGate.execute(sql)`。脚本与测试不得绕过 wrapper 直接调用 DuckDB 写 API(种子脚本 `scripts/gen_seed.py` 除外)。
440
+
441
+ ## Catalog / PII
442
+
443
+ `seed/catalog.json` (copied at `examples/catalog.json`): writable tables, allowed columns, `pii_columns`, optional `restricted_columns` (`id_card`, `card_number`).
444
+
445
+ - Write to PII / restricted columns → **BLOCK**
446
+ - `SELECT` of PII columns → **REQUIRE_APPROVAL** (not silent allow)
447
+ - `SELECT` of restricted columns → **BLOCK**
448
+
449
+ ## Audit
450
+
451
+ Every `check` / `exec` appends a JSON line to `.logs/audit.jsonl`:
452
+
453
+ `timestamp, agent, environment, sql, operation, table, estimated_rows, decision, rule_id`
454
+
455
+ `decision` in the file is `ALLOW` / `BLOCK` / `REQUIRE_APPROVAL`. `sql-write-gate audit` prints:
456
+
457
+ ```
458
+ TIME SOURCE OP TABLE VERDICT RULE
459
+ ---------------- ------ ------ ------ ------- --------------------
460
+ 2026-09-03 00:40 cli delete orders BLOCK delete_without_where
461
+ ```
462
+
463
+ VERDICT maps `REQUIRE_APPROVAL` → `APPROVAL` for the table only. Empty log prints `(no audit records)`.
464
+
465
+ ```bash
466
+ sql-write-gate audit
467
+ sql-write-gate audit --limit 50
468
+ sql-write-gate audit --audit-path /tmp/audit.jsonl
469
+ ```
470
+
471
+ ## 非目标
472
+
473
+ - 企业级 DQ / 数据质量平台、血缘 lineage
474
+ - ChatBI、SSO、多租户、计费
475
+ - LangGraph / CrewAI / 远程 MCP / 在线模型 / Web UI / PyPI publish
476
+ - spark-retail-dw 克隆、Spark 数仓、海量数据
477
+
478
+ Local, deterministic, screenshot-ready. DuckDB by default; Postgres via URL.
479
+
480
+ ## 许可
481
+
482
+ MIT。见 [LICENSE](LICENSE).