sql-write-gate 0.16.0__tar.gz → 0.16.1__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.
- {sql_write_gate-0.16.0/src/sql_write_gate.egg-info → sql_write_gate-0.16.1}/PKG-INFO +5 -1
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/README.md +4 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/pyproject.toml +1 -1
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1/src/sql_write_gate.egg-info}/PKG-INFO +5 -1
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/SOURCES.txt +2 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/__init__.py +1 -1
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/base.py +2 -2
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/duckdb.py +2 -2
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/approvals.py +3 -3
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/audit.py +6 -5
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/catalog.py +2 -2
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/cli.py +3 -3
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/config.py +2 -2
- sql_write_gate-0.16.1/src/write_gate/paths.py +99 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/wrapper.py +14 -6
- sql_write_gate-0.16.1/tests/test_installed_init_check.py +46 -0
- sql_write_gate-0.16.1/tests/test_paths_defaults.py +64 -0
- sql_write_gate-0.16.0/src/write_gate/paths.py +0 -20
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/LICENSE +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/setup.cfg +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/dependency_links.txt +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/entry_points.txt +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/requires.txt +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/top_level.txt +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/__main__.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/__init__.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/mysql.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/postgres.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/adapters/sqlite.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/cases.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/db.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/decision.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/engine.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/__init__.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/blast_radius.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/destructive.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/environment.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/freshness.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/pii.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/guards/schema.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/hooks.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/init.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/mcp_server.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/mcp_tools.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/parser.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/policy.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/proxy.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/templates/GETTING_STARTED.md +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/templates/catalog.json +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/write_gate/templates/policy.yaml +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_adapters.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_approvals.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_audit.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_blast_radius.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_demo_cases.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_destructive.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_environment.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_hooks.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_init.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_mcp.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_mcp_execute.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_mysql.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_mysql_live.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_policy.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_postgres_live.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_proxy.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_readme.py +0 -0
- {sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/tests/test_sqlite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-write-gate
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.1
|
|
4
4
|
Summary: Policy firewall for AI agents writing to databases (sql-write-gate)
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -322,6 +322,10 @@ Tagged **v0.11.0** with [CHANGELOG.md](CHANGELOG.md) and a GitHub Release. No Py
|
|
|
322
322
|
|
|
323
323
|
README badges (CI / Release / Python / License). Tagged **v0.13.0** with a GitHub Release. No PyPI. No product behavior change.
|
|
324
324
|
|
|
325
|
+
## v0.16.1 — installed-path defaults
|
|
326
|
+
|
|
327
|
+
After `pip install sql-write-gate` and `sql-write-gate init`, bare `sql-write-gate check "DELETE FROM orders"` uses `./policy.yaml` + `./catalog.json` (no `FileNotFoundError` on a fake `seed/catalog.json`).
|
|
328
|
+
|
|
325
329
|
## v0.16 — PyPI package name `sql-write-gate`
|
|
326
330
|
|
|
327
331
|
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`.
|
|
@@ -301,6 +301,10 @@ Tagged **v0.11.0** with [CHANGELOG.md](CHANGELOG.md) and a GitHub Release. No Py
|
|
|
301
301
|
|
|
302
302
|
README badges (CI / Release / Python / License). Tagged **v0.13.0** with a GitHub Release. No PyPI. No product behavior change.
|
|
303
303
|
|
|
304
|
+
## v0.16.1 — installed-path defaults
|
|
305
|
+
|
|
306
|
+
After `pip install sql-write-gate` and `sql-write-gate init`, bare `sql-write-gate check "DELETE FROM orders"` uses `./policy.yaml` + `./catalog.json` (no `FileNotFoundError` on a fake `seed/catalog.json`).
|
|
307
|
+
|
|
304
308
|
## v0.16 — PyPI package name `sql-write-gate`
|
|
305
309
|
|
|
306
310
|
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`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-write-gate
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.1
|
|
4
4
|
Summary: Policy firewall for AI agents writing to databases (sql-write-gate)
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -322,6 +322,10 @@ Tagged **v0.11.0** with [CHANGELOG.md](CHANGELOG.md) and a GitHub Release. No Py
|
|
|
322
322
|
|
|
323
323
|
README badges (CI / Release / Python / License). Tagged **v0.13.0** with a GitHub Release. No PyPI. No product behavior change.
|
|
324
324
|
|
|
325
|
+
## v0.16.1 — installed-path defaults
|
|
326
|
+
|
|
327
|
+
After `pip install sql-write-gate` and `sql-write-gate init`, bare `sql-write-gate check "DELETE FROM orders"` uses `./policy.yaml` + `./catalog.json` (no `FileNotFoundError` on a fake `seed/catalog.json`).
|
|
328
|
+
|
|
325
329
|
## v0.16 — PyPI package name `sql-write-gate`
|
|
326
330
|
|
|
327
331
|
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`.
|
|
@@ -52,10 +52,12 @@ tests/test_destructive.py
|
|
|
52
52
|
tests/test_environment.py
|
|
53
53
|
tests/test_hooks.py
|
|
54
54
|
tests/test_init.py
|
|
55
|
+
tests/test_installed_init_check.py
|
|
55
56
|
tests/test_mcp.py
|
|
56
57
|
tests/test_mcp_execute.py
|
|
57
58
|
tests/test_mysql.py
|
|
58
59
|
tests/test_mysql_live.py
|
|
60
|
+
tests/test_paths_defaults.py
|
|
59
61
|
tests/test_policy.py
|
|
60
62
|
tests/test_postgres_live.py
|
|
61
63
|
tests/test_proxy.py
|
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Mapping
|
|
8
8
|
|
|
9
|
-
from write_gate.paths import
|
|
9
|
+
from write_gate.paths import default_db_path
|
|
10
10
|
|
|
11
11
|
BACKEND_DUCKDB = "duckdb"
|
|
12
12
|
BACKEND_POSTGRES = "postgres"
|
|
@@ -98,4 +98,4 @@ def resolve_target(
|
|
|
98
98
|
env_url = env.get("DATABASE_URL")
|
|
99
99
|
if env_url:
|
|
100
100
|
return detect_backend(env_url), env_url
|
|
101
|
-
return BACKEND_DUCKDB, str(
|
|
101
|
+
return BACKEND_DUCKDB, str(default_db_path())
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import duckdb
|
|
8
8
|
|
|
9
9
|
from write_gate.adapters.base import BACKEND_DUCKDB, count_sql as _count_sql
|
|
10
|
-
from write_gate.paths import
|
|
10
|
+
from write_gate.paths import default_db_path
|
|
11
11
|
|
|
12
12
|
DIALECT = "duckdb"
|
|
13
13
|
BACKEND = BACKEND_DUCKDB
|
|
@@ -31,7 +31,7 @@ def count_sql(table: str, predicate: str | None) -> str:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def connect(db_path: Path | None = None, *, read_only: bool = False) -> duckdb.DuckDBPyConnection:
|
|
34
|
-
path = Path(db_path) if db_path else
|
|
34
|
+
path = Path(db_path) if db_path else default_db_path()
|
|
35
35
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
36
|
return duckdb.connect(str(path), read_only=read_only)
|
|
37
37
|
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
from write_gate.decision import Decision
|
|
13
|
-
from write_gate.paths import
|
|
13
|
+
from write_gate.paths import default_approvals_path, default_log_dir
|
|
14
14
|
|
|
15
15
|
STATUS_PENDING = "pending"
|
|
16
16
|
STATUS_APPROVED = "approved"
|
|
@@ -81,7 +81,7 @@ def _new_id() -> str:
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def _path(path: Path | str | None = None) -> Path:
|
|
84
|
-
return Path(path) if path else
|
|
84
|
+
return Path(path) if path else default_approvals_path()
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
def _load(path: Path) -> dict[str, dict[str, Any]]:
|
|
@@ -202,4 +202,4 @@ def mark_rejected(approval_id: str, path: Path | str | None = None) -> ApprovalR
|
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
def ensure_log_dir() -> None:
|
|
205
|
-
|
|
205
|
+
default_log_dir().mkdir(parents=True, exist_ok=True)
|
|
@@ -9,7 +9,7 @@ from typing import Any, Iterable
|
|
|
9
9
|
from zoneinfo import ZoneInfo
|
|
10
10
|
|
|
11
11
|
from write_gate.decision import ACTION_APPROVAL, Decision
|
|
12
|
-
from write_gate.paths import
|
|
12
|
+
from write_gate.paths import default_audit_path, default_log_dir
|
|
13
13
|
|
|
14
14
|
# Glanceable table times: UTC ISO on disk, Asia/Shanghai in `sql-write-gate audit`.
|
|
15
15
|
AUDIT_DISPLAY_TZ = ZoneInfo("Asia/Shanghai")
|
|
@@ -38,14 +38,14 @@ def append_audit(
|
|
|
38
38
|
"decision": decision.action,
|
|
39
39
|
"rule_id": decision.rule_id,
|
|
40
40
|
}
|
|
41
|
-
dest = Path(path) if path else
|
|
41
|
+
dest = Path(path) if path else default_audit_path()
|
|
42
42
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
43
43
|
with dest.open("a", encoding="utf-8") as fh:
|
|
44
44
|
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def read_audit(path: Path | None = None, limit: int = 20) -> list[dict[str, Any]]:
|
|
48
|
-
dest = Path(path) if path else
|
|
48
|
+
dest = Path(path) if path else default_audit_path()
|
|
49
49
|
if not dest.exists():
|
|
50
50
|
return []
|
|
51
51
|
lines = dest.read_text(encoding="utf-8").splitlines()
|
|
@@ -114,7 +114,8 @@ def format_audit_table(rows: Iterable[dict[str, Any]]) -> str:
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def ensure_log_dir() -> None:
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
log_dir = default_log_dir()
|
|
118
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
gitkeep = log_dir / ".gitkeep"
|
|
119
120
|
if not gitkeep.exists():
|
|
120
121
|
gitkeep.write_text("", encoding="utf-8")
|
|
@@ -8,7 +8,7 @@ from datetime import date, timedelta
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from write_gate.paths import
|
|
11
|
+
from write_gate.paths import default_catalog_path
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass(frozen=True)
|
|
@@ -65,7 +65,7 @@ def _table_spec(name: str, raw: dict[str, Any]) -> TableSpec:
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def load_catalog(path: Path | None = None) -> Catalog:
|
|
68
|
-
catalog_path = Path(path) if path else
|
|
68
|
+
catalog_path = Path(path) if path else default_catalog_path()
|
|
69
69
|
with catalog_path.open(encoding="utf-8") as fh:
|
|
70
70
|
raw = json.load(fh)
|
|
71
71
|
tables = {
|
|
@@ -15,7 +15,7 @@ from write_gate.approvals import (
|
|
|
15
15
|
)
|
|
16
16
|
from write_gate.audit import format_audit_table, read_audit
|
|
17
17
|
from write_gate.decision import ACTION_ALLOW, ACTION_APPROVAL, ACTION_BLOCK, Decision
|
|
18
|
-
from write_gate.paths import
|
|
18
|
+
from write_gate.paths import default_approvals_path, default_audit_path
|
|
19
19
|
from write_gate.wrapper import WriteGate
|
|
20
20
|
|
|
21
21
|
|
|
@@ -51,7 +51,7 @@ def _safe(value: object) -> str:
|
|
|
51
51
|
|
|
52
52
|
def _approvals_path(args: argparse.Namespace) -> Path:
|
|
53
53
|
raw = getattr(args, "approvals", None)
|
|
54
|
-
return Path(raw) if raw else
|
|
54
|
+
return Path(raw) if raw else default_approvals_path()
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def _gate_from_args(args: argparse.Namespace) -> WriteGate:
|
|
@@ -358,7 +358,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
358
358
|
args = parser.parse_args(argv)
|
|
359
359
|
|
|
360
360
|
if args.command == "audit":
|
|
361
|
-
path = Path(args.audit_path) if args.audit_path else
|
|
361
|
+
path = Path(args.audit_path) if args.audit_path else default_audit_path()
|
|
362
362
|
rows = read_audit(path, limit=args.limit)
|
|
363
363
|
sys.stdout.write(format_audit_table(rows) + "\n")
|
|
364
364
|
return 0
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
import yaml
|
|
10
10
|
|
|
11
|
-
from write_gate.paths import
|
|
11
|
+
from write_gate.paths import default_policy_path
|
|
12
12
|
|
|
13
13
|
VALID_RULES = {"allow", "block", "approval"}
|
|
14
14
|
VALID_OPS = ("select", "insert", "update", "delete", "ddl")
|
|
@@ -99,7 +99,7 @@ def policy_from_dict(raw: dict[str, Any] | None = None) -> Policy:
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def load_policy(path: Path | str | None = None) -> Policy:
|
|
102
|
-
policy_path = Path(path) if path else
|
|
102
|
+
policy_path = Path(path) if path else default_policy_path()
|
|
103
103
|
if not policy_path.exists():
|
|
104
104
|
return policy_from_dict(PRODUCTION_DEFAULTS)
|
|
105
105
|
with policy_path.open(encoding="utf-8") as fh:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Project paths.
|
|
2
|
+
|
|
3
|
+
Prefer cwd files written by ``sql-write-gate init`` (``./policy.yaml``,
|
|
4
|
+
``./catalog.json``). Only treat ``PACKAGE_DIR.parents[1]`` as a checkout root
|
|
5
|
+
when it looks like the repo (``pyproject.toml`` + ``seed/``). An installed
|
|
6
|
+
wheel must never invent a fake ``seed/`` under ``site-packages``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_checkout_root() -> Path | None:
|
|
17
|
+
# src layout: <root>/src/write_gate/paths.py → parents[1] == <root>
|
|
18
|
+
candidate = PACKAGE_DIR.parents[1]
|
|
19
|
+
if (candidate / "pyproject.toml").is_file() and (candidate / "seed").is_dir():
|
|
20
|
+
return candidate
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
CHECKOUT_ROOT = _find_checkout_root()
|
|
25
|
+
# Back-compat: many call sites expect PROJECT_ROOT. When installed, use cwd.
|
|
26
|
+
PROJECT_ROOT = CHECKOUT_ROOT if CHECKOUT_ROOT is not None else Path.cwd()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_policy_path() -> Path:
|
|
30
|
+
cwd_path = Path.cwd() / "policy.yaml"
|
|
31
|
+
if cwd_path.is_file():
|
|
32
|
+
return cwd_path
|
|
33
|
+
if CHECKOUT_ROOT is not None:
|
|
34
|
+
root_path = CHECKOUT_ROOT / "policy.yaml"
|
|
35
|
+
if root_path.is_file():
|
|
36
|
+
return root_path
|
|
37
|
+
return cwd_path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def default_catalog_path() -> Path:
|
|
41
|
+
cwd_path = Path.cwd() / "catalog.json"
|
|
42
|
+
if cwd_path.is_file():
|
|
43
|
+
return cwd_path
|
|
44
|
+
if CHECKOUT_ROOT is not None:
|
|
45
|
+
seed_cat = CHECKOUT_ROOT / "seed" / "catalog.json"
|
|
46
|
+
if seed_cat.is_file():
|
|
47
|
+
return seed_cat
|
|
48
|
+
return cwd_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_db_path() -> Path:
|
|
52
|
+
if CHECKOUT_ROOT is not None:
|
|
53
|
+
seed_db = CHECKOUT_ROOT / "seed" / "warehouse.duckdb"
|
|
54
|
+
if seed_db.is_file():
|
|
55
|
+
return seed_db
|
|
56
|
+
return Path.cwd() / "warehouse.duckdb"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def default_log_dir() -> Path:
|
|
60
|
+
if CHECKOUT_ROOT is not None:
|
|
61
|
+
return CHECKOUT_ROOT / ".logs"
|
|
62
|
+
return Path.cwd() / ".logs"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def default_audit_path() -> Path:
|
|
66
|
+
return default_log_dir() / "audit.jsonl"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def default_approvals_path() -> Path:
|
|
70
|
+
return default_log_dir() / "approvals.jsonl"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Checkout / demo helpers (tests and make demo run from a clone).
|
|
74
|
+
SEED_DIR = (CHECKOUT_ROOT / "seed") if CHECKOUT_ROOT is not None else (Path.cwd() / "seed")
|
|
75
|
+
EXAMPLES_DIR = (
|
|
76
|
+
(CHECKOUT_ROOT / "examples") if CHECKOUT_ROOT is not None else (Path.cwd() / "examples")
|
|
77
|
+
)
|
|
78
|
+
EXAMPLES_POLICY_PATH = EXAMPLES_DIR / "policy.yaml"
|
|
79
|
+
DEMO_POLICY_PATH = EXAMPLES_DIR / "policy.demo.yaml"
|
|
80
|
+
EXAMPLES_CATALOG_PATH = EXAMPLES_DIR / "catalog.json"
|
|
81
|
+
ORDERS_CSV = SEED_DIR / "orders.csv"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def __getattr__(name: str):
|
|
85
|
+
"""Lazy aliases so ``from write_gate.paths import CATALOG_PATH`` still works.
|
|
86
|
+
|
|
87
|
+
Prefer calling ``default_*_path()`` at use time when cwd may change.
|
|
88
|
+
"""
|
|
89
|
+
mapping = {
|
|
90
|
+
"POLICY_PATH": default_policy_path,
|
|
91
|
+
"CATALOG_PATH": default_catalog_path,
|
|
92
|
+
"DB_PATH": default_db_path,
|
|
93
|
+
"LOG_DIR": default_log_dir,
|
|
94
|
+
"AUDIT_PATH": default_audit_path,
|
|
95
|
+
"APPROVALS_PATH": default_approvals_path,
|
|
96
|
+
}
|
|
97
|
+
if name in mapping:
|
|
98
|
+
return mapping[name]()
|
|
99
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -24,7 +24,13 @@ from write_gate.catalog import Catalog, load_catalog
|
|
|
24
24
|
from write_gate.config import Policy, load_policy
|
|
25
25
|
from write_gate.decision import ACTION_ALLOW, ACTION_APPROVAL, Decision, Evidence
|
|
26
26
|
from write_gate.engine import evaluate
|
|
27
|
-
from write_gate.paths import
|
|
27
|
+
from write_gate.paths import (
|
|
28
|
+
default_approvals_path,
|
|
29
|
+
default_audit_path,
|
|
30
|
+
default_catalog_path,
|
|
31
|
+
default_db_path,
|
|
32
|
+
default_policy_path,
|
|
33
|
+
)
|
|
28
34
|
|
|
29
35
|
__all__ = ["WriteGate", "Evidence", "Decision"]
|
|
30
36
|
|
|
@@ -53,13 +59,15 @@ class WriteGate:
|
|
|
53
59
|
)
|
|
54
60
|
self.backend = backend
|
|
55
61
|
self.database = target
|
|
56
|
-
self.db_path = Path(target) if backend == BACKEND_DUCKDB else
|
|
57
|
-
self.catalog_path = Path(catalog_path) if catalog_path else
|
|
62
|
+
self.db_path = Path(target) if backend == BACKEND_DUCKDB else default_db_path()
|
|
63
|
+
self.catalog_path = Path(catalog_path) if catalog_path else default_catalog_path()
|
|
58
64
|
self.catalog = catalog or load_catalog(self.catalog_path)
|
|
59
|
-
self.policy_path = Path(policy_path) if policy_path else
|
|
65
|
+
self.policy_path = Path(policy_path) if policy_path else default_policy_path()
|
|
60
66
|
self.policy = policy or load_policy(self.policy_path)
|
|
61
|
-
self.audit_path = Path(audit_path) if audit_path else
|
|
62
|
-
self.approvals_path =
|
|
67
|
+
self.audit_path = Path(audit_path) if audit_path else default_audit_path()
|
|
68
|
+
self.approvals_path = (
|
|
69
|
+
Path(approvals_path) if approvals_path else default_approvals_path()
|
|
70
|
+
)
|
|
63
71
|
self.agent = agent
|
|
64
72
|
self._conn = conn
|
|
65
73
|
self._owns_conn = conn is None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Regression: pip-installed wheel + init + bare check must BLOCK, not FileNotFoundError."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_wheel_install_init_bare_check(tmp_path):
|
|
13
|
+
"""Build current tree into a wheel, install into a temp venv, init + bare check."""
|
|
14
|
+
outdir = tmp_path / "dist"
|
|
15
|
+
outdir.mkdir()
|
|
16
|
+
subprocess.check_call(
|
|
17
|
+
[sys.executable, "-m", "pip", "install", "-q", "build"],
|
|
18
|
+
cwd=str(ROOT),
|
|
19
|
+
)
|
|
20
|
+
subprocess.check_call(
|
|
21
|
+
[sys.executable, "-m", "build", "--wheel", "--outdir", str(outdir)],
|
|
22
|
+
cwd=str(ROOT),
|
|
23
|
+
)
|
|
24
|
+
wheels = sorted(outdir.glob("sql_write_gate-*.whl"))
|
|
25
|
+
assert wheels, "expected a wheel under temp dist/"
|
|
26
|
+
wheel = wheels[-1]
|
|
27
|
+
|
|
28
|
+
venv = tmp_path / "venv"
|
|
29
|
+
subprocess.check_call([sys.executable, "-m", "venv", str(venv)])
|
|
30
|
+
pip = venv / "bin" / "pip"
|
|
31
|
+
swg = venv / "bin" / "sql-write-gate"
|
|
32
|
+
subprocess.check_call([str(pip), "install", "-q", str(wheel)])
|
|
33
|
+
|
|
34
|
+
proj = tmp_path / "proj"
|
|
35
|
+
proj.mkdir()
|
|
36
|
+
subprocess.check_call([str(swg), "init", "--dir", str(proj)], cwd=str(proj))
|
|
37
|
+
proc = subprocess.run(
|
|
38
|
+
[str(swg), "check", "DELETE FROM orders"],
|
|
39
|
+
cwd=str(proj),
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
out = proc.stdout + proc.stderr
|
|
44
|
+
assert proc.returncode == 2, out
|
|
45
|
+
assert "delete_without_where" in out
|
|
46
|
+
assert "FileNotFoundError" not in out
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Default policy/catalog prefer cwd init files; installed layout has no fake seed/."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import write_gate.paths as paths
|
|
8
|
+
from write_gate.paths import (
|
|
9
|
+
CHECKOUT_ROOT,
|
|
10
|
+
default_approvals_path,
|
|
11
|
+
default_audit_path,
|
|
12
|
+
default_catalog_path,
|
|
13
|
+
default_db_path,
|
|
14
|
+
default_log_dir,
|
|
15
|
+
default_policy_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_checkout_root_detected_in_dev_tree():
|
|
20
|
+
assert CHECKOUT_ROOT is not None
|
|
21
|
+
assert (CHECKOUT_ROOT / "seed" / "catalog.json").is_file()
|
|
22
|
+
assert (CHECKOUT_ROOT / "pyproject.toml").is_file()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_find_checkout_root_rejects_site_packages_layout(tmp_path, monkeypatch):
|
|
26
|
+
fake_pkg = tmp_path / "lib" / "python3.13" / "site-packages" / "write_gate"
|
|
27
|
+
fake_pkg.mkdir(parents=True)
|
|
28
|
+
monkeypatch.setattr(paths, "PACKAGE_DIR", fake_pkg)
|
|
29
|
+
assert paths._find_checkout_root() is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cwd_init_files_win_over_seed(tmp_path, monkeypatch):
|
|
33
|
+
monkeypatch.chdir(tmp_path)
|
|
34
|
+
(tmp_path / "policy.yaml").write_text("environment: production\nrules: {}\nlimits: {}\n")
|
|
35
|
+
(tmp_path / "catalog.json").write_text(
|
|
36
|
+
'{"as_of_date":"2026-09-02","freshness_days":7,"tables":{}}'
|
|
37
|
+
)
|
|
38
|
+
assert default_policy_path() == tmp_path / "policy.yaml"
|
|
39
|
+
assert default_catalog_path() == tmp_path / "catalog.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_installed_layout_defaults_to_cwd(tmp_path, monkeypatch):
|
|
43
|
+
"""Simulate wheel install: package dir without repo seed/ above it."""
|
|
44
|
+
fake_pkg = tmp_path / "site-packages" / "write_gate"
|
|
45
|
+
fake_pkg.mkdir(parents=True)
|
|
46
|
+
monkeypatch.setattr(paths, "PACKAGE_DIR", fake_pkg)
|
|
47
|
+
monkeypatch.setattr(paths, "CHECKOUT_ROOT", None)
|
|
48
|
+
work = tmp_path / "proj"
|
|
49
|
+
work.mkdir()
|
|
50
|
+
monkeypatch.chdir(work)
|
|
51
|
+
(work / "policy.yaml").write_text("environment: production\nrules: {}\nlimits: {}\n")
|
|
52
|
+
(work / "catalog.json").write_text(
|
|
53
|
+
'{"as_of_date":"2026-09-02","freshness_days":7,"tables":{"orders":{"columns":{},'
|
|
54
|
+
'"allowed_write_columns":[],"pii_columns":[],"restricted_columns":[]}}}'
|
|
55
|
+
)
|
|
56
|
+
assert default_policy_path() == work / "policy.yaml"
|
|
57
|
+
assert default_catalog_path() == work / "catalog.json"
|
|
58
|
+
assert default_db_path() == work / "warehouse.duckdb"
|
|
59
|
+
assert default_log_dir() == work / ".logs"
|
|
60
|
+
assert default_audit_path() == work / ".logs" / "audit.jsonl"
|
|
61
|
+
assert default_approvals_path() == work / ".logs" / "approvals.jsonl"
|
|
62
|
+
# Must not point into site-packages parents
|
|
63
|
+
assert "site-packages" not in str(default_catalog_path())
|
|
64
|
+
assert "site-packages" not in str(default_policy_path())
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""Project paths. Warehouse and catalog live under seed/."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
6
|
-
PROJECT_ROOT = PACKAGE_DIR.parents[1]
|
|
7
|
-
SEED_DIR = PROJECT_ROOT / "seed"
|
|
8
|
-
EXAMPLES_DIR = PROJECT_ROOT / "examples"
|
|
9
|
-
LOG_DIR = PROJECT_ROOT / ".logs"
|
|
10
|
-
|
|
11
|
-
DB_PATH = SEED_DIR / "warehouse.duckdb"
|
|
12
|
-
CATALOG_PATH = SEED_DIR / "catalog.json"
|
|
13
|
-
ORDERS_CSV = SEED_DIR / "orders.csv"
|
|
14
|
-
|
|
15
|
-
POLICY_PATH = PROJECT_ROOT / "policy.yaml"
|
|
16
|
-
EXAMPLES_POLICY_PATH = EXAMPLES_DIR / "policy.yaml"
|
|
17
|
-
DEMO_POLICY_PATH = EXAMPLES_DIR / "policy.demo.yaml"
|
|
18
|
-
EXAMPLES_CATALOG_PATH = EXAMPLES_DIR / "catalog.json"
|
|
19
|
-
AUDIT_PATH = LOG_DIR / "audit.jsonl"
|
|
20
|
-
APPROVALS_PATH = LOG_DIR / "approvals.jsonl"
|
|
File without changes
|
|
File without changes
|
{sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sql_write_gate-0.16.0 → sql_write_gate-0.16.1}/src/sql_write_gate.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|