sql-write-gate 0.16.0__py3-none-any.whl
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.
- sql_write_gate-0.16.0.dist-info/METADATA +482 -0
- sql_write_gate-0.16.0.dist-info/RECORD +42 -0
- sql_write_gate-0.16.0.dist-info/WHEEL +5 -0
- sql_write_gate-0.16.0.dist-info/entry_points.txt +2 -0
- sql_write_gate-0.16.0.dist-info/licenses/LICENSE +21 -0
- sql_write_gate-0.16.0.dist-info/top_level.txt +1 -0
- write_gate/__init__.py +7 -0
- write_gate/__main__.py +6 -0
- write_gate/adapters/__init__.py +33 -0
- write_gate/adapters/base.py +101 -0
- write_gate/adapters/duckdb.py +41 -0
- write_gate/adapters/mysql.py +115 -0
- write_gate/adapters/postgres.py +96 -0
- write_gate/adapters/sqlite.py +116 -0
- write_gate/approvals.py +205 -0
- write_gate/audit.py +120 -0
- write_gate/cases.py +30 -0
- write_gate/catalog.py +79 -0
- write_gate/cli.py +397 -0
- write_gate/config.py +117 -0
- write_gate/db.py +5 -0
- write_gate/decision.py +143 -0
- write_gate/engine.py +150 -0
- write_gate/guards/__init__.py +17 -0
- write_gate/guards/blast_radius.py +73 -0
- write_gate/guards/destructive.py +85 -0
- write_gate/guards/environment.py +41 -0
- write_gate/guards/freshness.py +102 -0
- write_gate/guards/pii.py +84 -0
- write_gate/guards/schema.py +147 -0
- write_gate/hooks.py +360 -0
- write_gate/init.py +84 -0
- write_gate/mcp_server.py +71 -0
- write_gate/mcp_tools.py +197 -0
- write_gate/parser.py +367 -0
- write_gate/paths.py +20 -0
- write_gate/policy.py +49 -0
- write_gate/proxy.py +273 -0
- write_gate/templates/GETTING_STARTED.md +20 -0
- write_gate/templates/catalog.json +32 -0
- write_gate/templates/policy.yaml +12 -0
- write_gate/wrapper.py +207 -0
|
@@ -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
|
+
[](https://github.com/tangyf07/sql-write-gate/actions/workflows/ci.yml)
|
|
25
|
+
[](https://github.com/tangyf07/sql-write-gate/releases/latest)
|
|
26
|
+
[](https://www.python.org/downloads/)
|
|
27
|
+
[](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).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
sql_write_gate-0.16.0.dist-info/licenses/LICENSE,sha256=iWczeAI5ZeERk1CU2hGlzVbtgkpBzMLijIaxpW-Id3U,1084
|
|
2
|
+
write_gate/__init__.py,sha256=ikyH_Rxxw3qJsElyLtS2IO0q8omyVNafgf6zaH1n0tE,239
|
|
3
|
+
write_gate/__main__.py,sha256=6ijCRJw1h1ZY5znTSdTcMIFJZziN5R7WgUcfqgGV1bw,181
|
|
4
|
+
write_gate/approvals.py,sha256=tn9uJsk48q8zWigMd_jLipL2DzioAEOU0xqPjHFI7SU,6023
|
|
5
|
+
write_gate/audit.py,sha256=AxGoYMuzepwBDcP5FHRJGAwPJkbRePz1_I6h03Tl6IY,3782
|
|
6
|
+
write_gate/cases.py,sha256=NDbDgVp6vfkVca9O9urcqvGvZl5nVWTTkyS88THKpBk,975
|
|
7
|
+
write_gate/catalog.py,sha256=MpKd62-XNdTpDOs6sVbGz6-6ghBp4ZvV2B1UlG1mT1U,2519
|
|
8
|
+
write_gate/cli.py,sha256=uN_XPgy4GyRIg59NZzII5jgWTJA3TPYU34T52EIsb58,12665
|
|
9
|
+
write_gate/config.py,sha256=-xB7EoSgYluTziREAokKdKCtnEk7vac-75sMLddFiaM,3371
|
|
10
|
+
write_gate/db.py,sha256=1oIyC_Eb-DweHZ7jgmRiL1tu2s01904IoTlBOH7tp4Y,196
|
|
11
|
+
write_gate/decision.py,sha256=pfMXa-6IGjVmXkM1JbEqztnYkrhJrmZTJFRczolX2mY,3632
|
|
12
|
+
write_gate/engine.py,sha256=fQMeBEl36Ykk_OzPpTpVq3J-2epd_VSVVhDxv3aiRZU,4469
|
|
13
|
+
write_gate/hooks.py,sha256=19xHf3FU6k-0u4O79J2D-L-H-vYj39wcZe16tp6O0Es,9918
|
|
14
|
+
write_gate/init.py,sha256=I1gIu2aFihAJKVBTLtBGMRyJlxQco1m_MMssyBBDsd0,2793
|
|
15
|
+
write_gate/mcp_server.py,sha256=cJBSTIu7MgZVKqQOBFKCj-j20IBot8vlCLz66D8UaGA,1818
|
|
16
|
+
write_gate/mcp_tools.py,sha256=Z2jJZX9oj6tj-KeLTNjjjrSiN9My4_3maMOgTp0JfD4,5571
|
|
17
|
+
write_gate/parser.py,sha256=6CBMaUYmcWGZVzvtUb_czcw3QiMBawQ2LtIBLG6m0jc,11142
|
|
18
|
+
write_gate/paths.py,sha256=1HqeUD4feVgSGZ3ZYrK8Tcm4CH7-IuM3MwhJCLNBOWI,683
|
|
19
|
+
write_gate/policy.py,sha256=YEaOtK66BNqEomQlba7KObNlIk9my9FqvC9fzAQXylg,1088
|
|
20
|
+
write_gate/proxy.py,sha256=5KBYcF0lJEyyNXbIAP0I_61UGkHSbFP3cQQ-re-FSDA,8377
|
|
21
|
+
write_gate/wrapper.py,sha256=MuMC9oZcGqvSYUaDQVXFLCkjbPh7Zwy2lvGM4jq9tUw,7561
|
|
22
|
+
write_gate/adapters/__init__.py,sha256=TGbFXaZS2uvw2k82Nvk6EHqCdL7nYZ0Z9ygxLImIrAI,734
|
|
23
|
+
write_gate/adapters/base.py,sha256=rhOhAWsgds5xN_4aK8l-H6zp9w6RawDnXT6qpr5YmQs,2927
|
|
24
|
+
write_gate/adapters/duckdb.py,sha256=djUOQrAW7TlqfWOVREl_g73hPYLcfKuFJJIYCP_HAAg,1176
|
|
25
|
+
write_gate/adapters/mysql.py,sha256=CywUjDAyUHX59_bLesVq-lxzNViH7rLyu4i6Wlc2McE,3601
|
|
26
|
+
write_gate/adapters/postgres.py,sha256=yV1sDU7kl6U97a0mX3GpxMrhoSUth023ZbH52PaTtVg,2796
|
|
27
|
+
write_gate/adapters/sqlite.py,sha256=Pw9X5gzMaT6sr3fTi91fJTAx_YEUn5ZL5e3ExzCzNws,3491
|
|
28
|
+
write_gate/guards/__init__.py,sha256=9cwfey2-k1Rap-m33t8yWaOYmprc9jlO8fBu1qexv0c,552
|
|
29
|
+
write_gate/guards/blast_radius.py,sha256=RTCUGLG5Lovmwd3TUX2Js30RYYixAvormOZqbGAGU88,2319
|
|
30
|
+
write_gate/guards/destructive.py,sha256=nZCZzMPS5k0UZHHOV4jhhuG9_1kRz2-MCtOTiuAcICA,2641
|
|
31
|
+
write_gate/guards/environment.py,sha256=Xtoyo11-HUec32KbgW_rgVQS1t3rxnJaTwPHq2d4BcA,1223
|
|
32
|
+
write_gate/guards/freshness.py,sha256=4FO9aDeiEJHQKPW718RCtRhyCKfeKHkWpxaa2zpIiXY,3290
|
|
33
|
+
write_gate/guards/pii.py,sha256=_ffHRf8ImBzUaPkkiBRUZrjzE4XzalY23WX7iwN21y4,2977
|
|
34
|
+
write_gate/guards/schema.py,sha256=oDdZZho_2hSNcuc_-GxdXxuU-R-_EUxV7VCVY7go92g,4696
|
|
35
|
+
write_gate/templates/GETTING_STARTED.md,sha256=gizCBq2CHhpBNY685tXj44kNa5gCB41DurwIfrsj8N4,641
|
|
36
|
+
write_gate/templates/catalog.json,sha256=6GpwWcHe80irEq3BCPr1plnYIGslzlSDBO6YoO7V8l8,624
|
|
37
|
+
write_gate/templates/policy.yaml,sha256=sdYvSBIyo2TR94XbvA9XYAjlKrn9nW4uT9nOfRJ7YlY,319
|
|
38
|
+
sql_write_gate-0.16.0.dist-info/METADATA,sha256=WEQmYPOSey_9q-1m3kOskGiPW7RGy-cVLESIEiNC-tY,19247
|
|
39
|
+
sql_write_gate-0.16.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
40
|
+
sql_write_gate-0.16.0.dist-info/entry_points.txt,sha256=uEpjUBFvY4pXaC4_FE9fCJakPFLKmorToaISA4oPOpQ,55
|
|
41
|
+
sql_write_gate-0.16.0.dist-info/top_level.txt,sha256=98PGib1_VCsBS95jlQkVeGun6Ti0PX1egHu0MlJBKQg,11
|
|
42
|
+
sql_write_gate-0.16.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
write_gate
|
write_gate/__init__.py
ADDED
write_gate/__main__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Database adapters. DuckDB is default; Postgres/MySQL/SQLite are selected from a URL."""
|
|
2
|
+
|
|
3
|
+
from write_gate.adapters.base import (
|
|
4
|
+
BACKEND_DUCKDB,
|
|
5
|
+
BACKEND_MYSQL,
|
|
6
|
+
BACKEND_POSTGRES,
|
|
7
|
+
BACKEND_SQLITE,
|
|
8
|
+
count_sql,
|
|
9
|
+
detect_backend,
|
|
10
|
+
is_mysql_url,
|
|
11
|
+
is_postgres_url,
|
|
12
|
+
is_sqlite_url,
|
|
13
|
+
resolve_target,
|
|
14
|
+
sqlglot_dialect,
|
|
15
|
+
)
|
|
16
|
+
from write_gate.adapters.duckdb import ORDERS_DDL, connect, execute_user_sql
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BACKEND_DUCKDB",
|
|
20
|
+
"BACKEND_MYSQL",
|
|
21
|
+
"BACKEND_POSTGRES",
|
|
22
|
+
"BACKEND_SQLITE",
|
|
23
|
+
"ORDERS_DDL",
|
|
24
|
+
"connect",
|
|
25
|
+
"count_sql",
|
|
26
|
+
"detect_backend",
|
|
27
|
+
"execute_user_sql",
|
|
28
|
+
"is_mysql_url",
|
|
29
|
+
"is_postgres_url",
|
|
30
|
+
"is_sqlite_url",
|
|
31
|
+
"resolve_target",
|
|
32
|
+
"sqlglot_dialect",
|
|
33
|
+
]
|