From fd84a9f8759c9123d80cc845827db8ee12822728 Mon Sep 17 00:00:00 2001 From: Yuanfei Tang Date: Tue, 8 Sep 2026 19:38:54 +0800 Subject: [PATCH 1/2] feat(seal): make seal runs exactly 4 core ALLOW/BLOCK cases Add scripts/seal.py + make seal for legal/PII/schema/expired in fixed order with ALLOW|BLOCK, rule_id, and evidence JSON. Document in SEAL.md and link from README top. Keep make demo walkthrough unchanged. --- Makefile | 8 +++-- README.md | 7 ++++- SEAL.md | 22 ++++++++++++++ scripts/seal.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 SEAL.md create mode 100644 scripts/seal.py diff --git a/Makefile b/Makefile index 3ad21df..fa76b76 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BIN := $(VENV)/bin PIP := $(BIN)/pip PY := $(BIN)/python -.PHONY: install seed test demo clean +.PHONY: install seed test demo seal clean install: $(VENV)/pyvenv.cfg $(PIP) install -e ".[dev,postgres,mysql]" @@ -23,8 +23,12 @@ demo: seed $(PY) scripts/demo.py $(PY) scripts/demo_walkthrough.py +# Exactly 4 core cases; exit non-zero on mismatch. See SEAL.md. +seal: seed + $(PY) scripts/seal.py + clean: rm -rf $(VENV) src/write_gate.egg-info .pytest_cache rm -f seed/warehouse.duckdb seed/warehouse.duckdb.wal seed/orders.csv rm -f .logs/*.jsonl - find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true \ No newline at end of file diff --git a/README.md b/README.md index 6081fb6..9105479 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ 一句话:**SQLGuard**(仓库名 `sql-write-gate`)= 面向 AI Agent 的 **SQL 安全执行网关**;简历项目名建议写 **SQLGuard**。 +**Seal (3 min):** [SEAL.md](SEAL.md) — `make seal` runs exactly 4 core cases and prints `ALLOW`/`BLOCK` + `rule_id` + evidence. +Expected: legal→ALLOW/ok · PII→BLOCK/pii_column · schema→BLOCK/schema_hallucination · expired→BLOCK/expired_partition. +非生产唯一边界 — not the sole production DB security boundary. + + - [GameStream](https://github.com/tangyf07/GameStream)(指标 / ADS) - [DataPilot](https://github.com/tangyf07/DataPilot)(问数 → Text2SQL → 出站门禁) - 本仓:SQLGuard 执行前 BLOCK / EXECUTE @@ -58,7 +63,7 @@ sql-write-gate check "DELETE FROM users" ## Try it ```bash -make install && make test && make demo +make install && make test && make seal # or: make demo sql-write-gate check "DELETE FROM orders" # → BLOCKED delete_without_where sql-write-gate datapilot --json "SELECT o.order_id FROM orders o CROSS JOIN orders p" diff --git a/SEAL.md b/SEAL.md new file mode 100644 index 0000000..688920a --- /dev/null +++ b/SEAL.md @@ -0,0 +1,22 @@ +# SQLGuard seal (3 minutes) + +**What:** Deterministic SQL write gate for AI agents — ALLOW/BLOCK with rule_id + evidence. No LLM. + +**One command:** + +```bash +make seal +``` + +**Expected 4 outcomes (stable order):** + +| # | Case | Verdict | rule_id | +|---|------|---------|--------| +| 1 | legal write | ALLOW | `ok` | +| 2 | PII write | BLOCK | `pii_column` | +| 3 | schema mismatch | BLOCK | `schema_hallucination` | +| 4 | expired partition | BLOCK | `expired_partition` | + +Stdout prints `ALLOW` or `BLOCK`, `rule_id`, and structured evidence JSON. Non-zero exit on mismatch. + +**Boundary (honest):** 非生产唯一边界 — not the sole production DB security boundary. Pair with least-privilege DB roles, network isolation, and human review. Broader demo: `make demo`. DataPilot contract wording stays BLOCK / EXECUTE. \ No newline at end of file diff --git a/scripts/seal.py b/scripts/seal.py new file mode 100644 index 0000000..f206315 --- /dev/null +++ b/scripts/seal.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""SQLGuard seal: exactly four core cases (ALLOW/BLOCK + rule_id + evidence).""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) + +from write_gate.cases import ( # noqa: E402 + EXPIRED_WRITE_SQL, + LEGAL_WRITE_SQL, + PII_WRITE_SQL, + SCHEMA_MISMATCH_SQL, +) +from write_gate.paths import DB_PATH, DEMO_POLICY_PATH # noqa: E402 +from write_gate.wrapper import WriteGate # noqa: E402 + +# Stable seal order — do not reorder. +CASES = [ + ("legal_write", LEGAL_WRITE_SQL, True, "ok"), + ("pii_write", PII_WRITE_SQL, False, "pii_column"), + ("schema_mismatch", SCHEMA_MISMATCH_SQL, False, "schema_hallucination"), + ("expired_partition", EXPIRED_WRITE_SQL, False, "expired_partition"), +] + + +def main() -> int: + if len(CASES) != 4: + print("SEAL_FAIL: expected exactly 4 cases", file=sys.stderr) + return 1 + + rc = 0 + audit_path = ROOT / ".logs" / "seal_audit.jsonl" + if audit_path.exists(): + audit_path.unlink() + + with WriteGate( + db_path=DB_PATH, + policy_path=DEMO_POLICY_PATH, + audit_path=audit_path, + agent="seal", + actor="sqlguard-seal", + model_id="seal-offline", + prompt_summary="make seal four core cases", + ) as gate: + for name, sql, expect_allow, expect_rule in CASES: + evidence, _result = gate.execute(sql) + verdict = "ALLOW" if evidence.allowed else "BLOCK" + payload = { + "case": name, + "verdict": verdict, + "rule_id": evidence.rule_id, + "evidence": evidence.to_dict(), + } + print(json.dumps(payload, ensure_ascii=False)) + print(f"{verdict} rule_id={evidence.rule_id} case={name}") + if evidence.allowed != expect_allow or evidence.rule_id != expect_rule: + print( + f"SEAL_MISMATCH: case={name} expected verdict=" + f"{'ALLOW' if expect_allow else 'BLOCK'} rule_id={expect_rule} " + f"got verdict={verdict} rule_id={evidence.rule_id}", + file=sys.stderr, + ) + rc = 1 + + if rc == 0: + print("seal: 4/4 cases matched") + else: + print("seal: FAILED", file=sys.stderr) + return rc + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file From c678232397a854c54b22037603ecbb14d12534de Mon Sep 17 00:00:00 2001 From: Yuanfei Tang Date: Tue, 8 Sep 2026 19:40:36 +0800 Subject: [PATCH 2/2] fix(seal): clear demo order_ids before seal runs Keep seal idempotent when re-run without a fresh seed. --- scripts/seal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/seal.py b/scripts/seal.py index f206315..138a92b 100644 --- a/scripts/seal.py +++ b/scripts/seal.py @@ -49,6 +49,8 @@ def main() -> int: model_id="seal-offline", prompt_summary="make seal four core cases", ) as gate: + # Idempotent vs prior seal runs (seed rows use ids 1..120). + gate.conn.execute("DELETE FROM orders WHERE order_id IN (900001, 900002, 900003, 900004)") for name, sql, expect_allow, expect_rule in CASES: evidence, _result = gate.execute(sql) verdict = "ALLOW" if evidence.allowed else "BLOCK"