Skip to content

Commit 088ac35

Browse files
chore(release): DeepSQL v1.2.0 (#69)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Weekly product cut **v1.2.0** — schema-scoped chat access policies, admin profile switch, ACME ERP multi-schema fixture, and fixes landed on `main` since `v1.1.0`. ## Release contents - Bump `backend/pom.xml` → `1.2.0` - `CHANGELOG.md` + `docs/releases/RELEASE_NOTES-v1.2.0.md` - `docs/oss-ux/RELEASE.md` current version table - New gate: `scripts/self-host/e2e-multischema-check.py` ## Verification (cloud VM) | Check | Result | |-------|--------| | Policy unit tests (27) | PASS | | `e2e-multischema-check.py` | PASS — admin sees 7 schemas; impersonated `marts-editor` sees marts tables only | | `e2e-agent-check.py` (ACME ERP) | PASS — `AGENT_OK` + `DASH_OK` | | `./scripts/release/build-artifacts.sh v1.2.0` | PASS — `sha256sum -c SHA256SUMS` | ## After merge ```bash git tag -a v1.2.0 -m "DeepSQL v1.2.0" git push origin v1.2.0 ``` Tag push triggers `.github/workflows/release.yml` to publish GitHub Release artifacts. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent e7834df commit 088ac35

5 files changed

Lines changed: 266 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ for product tags (`vMAJOR.MINOR.PATCH`).
88

99
Product releases follow a **weekly cadence** (Saturday 09:00 America/Los_Angeles). See `docs/oss-ux/RELEASE.md`.
1010

11+
## [1.2.0] — 2026-08-19
12+
13+
### Added
14+
15+
- Schema-scoped chat access policies with plain-English rules (`allowedSchemas`, column/table deny lists).
16+
- Admin profile switch (“View as”) for policy validation (`/admin/impersonate`).
17+
- ACME ERP multi-schema Postgres fixture (`acme_erp`: `crm`, `sales`, `finance`, `inventory`, `hr`, `marts`) and `scripts/seed-acme-erp.sh`.
18+
- Multi-schema E2E gate: `scripts/self-host/e2e-multischema-check.py`.
19+
20+
### Fixed
21+
22+
- Postgres `getTableColumns` binds the caller schema (#60).
23+
- Dashboard loading states (#61).
24+
- SQL editor guard bypasses closed (#63).
25+
- `COMMENT` / `CALL` table names no longer treated as mutations (#64).
26+
- ReDoS-safe parsing for chat-access-policy deny/allow regexes (#65).
27+
- Hermes agent + web UI pinned to release tags (#68).
28+
29+
### Changed
30+
31+
- Schema API (`/objects`, `/schema`, table indexes/stats) filters by per-user policy.
32+
- `@deepsql/mcp` remains `0.27.0` for this cut.
33+
1134
## [1.1.0] — 2026-08-15
1235

1336
### Added
@@ -59,5 +82,6 @@ First public OSS release.
5982
- Residual high-severity items tracked in `docs/oss-ux/OSS_SECURITY_REVIEW.md` (IDOR sweep, SET preamble allowlist, SSRF hardening, share-password defaults) are deferred past this cut.
6083
- Primary distribution path remains `docker compose up --build` (no pre-built container registry in this release).
6184

85+
[1.2.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.2.0
6286
[1.1.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.1.0
6387
[1.0.0]: https://github.com/DeepSQLAI/deepsql/releases/tag/v1.0.0

backend/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<groupId>com.dbaagent</groupId>
1616
<artifactId>dba-agent-backend</artifactId>
17-
<version>1.1.0</version>
17+
<version>1.2.0</version>
1818
<name>DBA Agent Backend</name>
1919
<description>Backend service for DBA Agent with secure credential storage and schema analysis</description>
2020

docs/oss-ux/RELEASE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Cron for the weekly automation: `CRON_TZ=America/Los_Angeles 0 9 * * 6`.
1919

2020
| Surface | Where | Current |
2121
|---------|-------|---------|
22-
| Product / Git tag | `vMAJOR.MINOR.PATCH` | `v1.1.0` |
23-
| Backend JAR | `backend/pom.xml` `<version>` | `1.1.0` |
22+
| Product / Git tag | `vMAJOR.MINOR.PATCH` | `v1.2.0` |
23+
| Backend JAR | `backend/pom.xml` `<version>` | `1.2.0` |
2424
| Frontend package | root `package.json` | `0.1.0` (internal) |
2525
| MCP / CLI npm | `mcp/package.json` | `0.27.0` |
2626

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# DeepSQL v1.2.0
2+
3+
**Weekly product cut** — schema-scoped access policies, admin profile switch, and multi-schema hardening on top of `v1.1.0`.
4+
5+
## Highlights
6+
7+
- **Schema-scoped chat access policies** — plain-English rules resolve to allowed schemas plus table/column deny lists; schema APIs and query guardrails enforce them per user.
8+
- **Admin profile switch** — “View as” another user to validate policies without sharing credentials (`/admin/impersonate`).
9+
- **ACME ERP fixture** — six-schema Postgres database (`crm`, `sales`, `finance`, `inventory`, `hr`, `marts`) for policy and Brain tests; seed with `scripts/seed-acme-erp.sh`.
10+
- **Postgres introspection fix**`getTableColumns` respects the caller’s schema (#60).
11+
- **Editor + security fixes** — SQL guard bypasses closed (#63); `COMMENT`/`CALL` no longer misclassified as mutations (#64).
12+
- **Dashboard UX** — loading states (#61); Hermes agent/web UI pinned to release tags (#68).
13+
14+
## Install / upgrade
15+
16+
```bash
17+
git clone https://github.com/DeepSQLAI/deepsql.git
18+
cd deepsql
19+
git checkout v1.2.0
20+
cp .env.example .env
21+
# set SECURITY_JWT_SECRET, ENCRYPTION_KEY (or ENCRYPTION_KEYS), and DEEPSQL_CHAT_* in .env
22+
docker compose up --build -d
23+
```
24+
25+
From `v1.1.0`: pull/checkout `v1.2.0` and rebuild Compose (or swap JAR + frontend tarball from this Release). Schema is managed by `ddl-auto=update`; no new hand SQL is required for this cut.
26+
27+
Air-gapped: download artifacts below, verify `SHA256SUMS`, follow `docs/oss-ux/RELEASE.md`.
28+
29+
## Artifacts in this release
30+
31+
| File | Contents |
32+
|------|----------|
33+
| `deepsql-1.2.0-source.tar.gz` / `.zip` | Source tree at this tag |
34+
| `dba-agent-backend-1.2.0.jar` | Spring Boot executable |
35+
| `deepsql-frontend-1.2.0.tar.gz` | Production static UI |
36+
| `deepsql-mcp-0.27.0.tgz` | CLI + MCP server package (unchanged npm line) |
37+
| `sbom-*.cdx.json` | CycloneDX SBOMs |
38+
| `SHA256SUMS` / `SHA512SUMS` | Integrity hashes |
39+
| `manifest.json` | Machine-readable inventory |
40+
41+
```bash
42+
sha256sum -c SHA256SUMS
43+
```
44+
45+
## Verify
46+
47+
```bash
48+
python3 scripts/self-host/e2e-agent-check.py
49+
python3 scripts/self-host/e2e-multischema-check.py
50+
```
51+
52+
Expect `AGENT_OK True`, `DASH_OK True`, and `✓ Multi-schema E2E OK`.
53+
54+
## Cadence
55+
56+
Product releases are cut **weekly, Saturday 09:00 America/Los_Angeles**. See `docs/oss-ux/RELEASE.md` and `docs/oss-ux/WEEKLY_RELEASE_AUTOMATION.md`.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""Multi-schema + chat-access-policy E2E checks for ACME ERP fixture.
3+
4+
Usage (repo root, stack running with auth):
5+
python3 scripts/self-host/e2e-multischema-check.py
6+
"""
7+
from __future__ import annotations
8+
9+
import json
10+
import os
11+
import sys
12+
import urllib.error
13+
import urllib.request
14+
from http.cookiejar import CookieJar
15+
from pathlib import Path
16+
from urllib.request import HTTPCookieProcessor, build_opener
17+
18+
ROOT = Path(__file__).resolve().parents[2]
19+
ENV = ROOT / ".env"
20+
21+
EXPECTED_SCHEMAS = {"crm", "sales", "finance", "inventory", "hr", "marts", "public"}
22+
MARTS_ONLY = {"marts"}
23+
24+
25+
def load_env(path: Path) -> dict[str, str]:
26+
out: dict[str, str] = {}
27+
if not path.exists():
28+
return out
29+
for line in path.read_text().splitlines():
30+
line = line.strip()
31+
if not line or line.startswith("#") or "=" not in line:
32+
continue
33+
k, v = line.split("=", 1)
34+
out[k.strip()] = v.strip().strip('"').strip("'")
35+
return out
36+
37+
38+
def main() -> int:
39+
env = {**load_env(ENV), **os.environ}
40+
email = env.get("DEEPSQL_INITIAL_ADMIN_EMAIL") or env.get("DEEPSQL_SMOKE_EMAIL")
41+
password = env.get("DEEPSQL_INITIAL_ADMIN_PASSWORD") or env.get("DEEPSQL_SMOKE_PASSWORD")
42+
if not email or not password:
43+
print("Missing admin credentials in .env", file=sys.stderr)
44+
return 1
45+
46+
frontend = f"http://localhost:{env.get('DEEPSQL_FRONTEND_PORT', '3000')}"
47+
backend = f"http://localhost:{env.get('DEEPSQL_BACKEND_PORT', '8080')}/api"
48+
acme_name = env.get("DEEPSQL_ACME_CONNECTION_NAME", "ACME ERP (Multi-Schema)")
49+
50+
opener = build_opener(HTTPCookieProcessor(CookieJar()))
51+
52+
def req(url: str, data=None, method: str | None = None):
53+
body = None
54+
headers: dict[str, str] = {}
55+
if data is not None:
56+
body = json.dumps(data).encode()
57+
headers["Content-Type"] = "application/json"
58+
m = method or ("POST" if data is not None else "GET")
59+
r = urllib.request.Request(url, data=body, headers=headers, method=m)
60+
with opener.open(r, timeout=120) as resp:
61+
raw = resp.read().decode() or "null"
62+
return json.loads(raw)
63+
64+
print("→ login")
65+
try:
66+
req(f"{frontend}/api/auth/login", {"email": email, "password": password})
67+
except Exception:
68+
req(f"{backend}/auth/login", {"email": email, "password": password})
69+
70+
print("→ resolve ACME connection")
71+
conns = req(f"{backend}/connections")
72+
items = conns if isinstance(conns, list) else (conns.get("connections") or conns.get("items") or [])
73+
conn_id = None
74+
for c in items:
75+
if c.get("connectionName") == acme_name:
76+
conn_id = c.get("connectionId") or c.get("id")
77+
break
78+
if not conn_id:
79+
payload = {
80+
"connectionName": acme_name,
81+
"dbType": "postgres",
82+
"host": "127.0.0.1",
83+
"port": 5432,
84+
"database": "acme_erp",
85+
"username": "postgres",
86+
"password": env.get("DB_PASSWORD", "postgres"),
87+
"sslEnabled": False,
88+
}
89+
saved = req(f"{backend}/connections", payload)
90+
conn_id = saved.get("connectionId") or saved.get("id")
91+
if not conn_id:
92+
print("FAIL: no ACME connection", file=sys.stderr)
93+
return 1
94+
print(f" connection {conn_id}")
95+
96+
print("→ admin schema objects (expect all business schemas)")
97+
obj_resp = req(f"{backend}/connections/{conn_id}/objects")
98+
objects = obj_resp.get("objects") if isinstance(obj_resp, dict) else obj_resp
99+
if not isinstance(objects, list):
100+
print(f"FAIL: unexpected objects payload: {obj_resp!r:.200}")
101+
return 1
102+
schemas = {o.get("schema") for o in objects if o.get("schema")}
103+
missing = EXPECTED_SCHEMAS - schemas
104+
if missing:
105+
print(f"FAIL: admin missing schemas {sorted(missing)}; got {sorted(schemas)}")
106+
return 1
107+
print(f" OK schemas={sorted(s for s in schemas if s in EXPECTED_SCHEMAS)}")
108+
109+
print("→ ensure marts-editor user exists")
110+
users = req(f"{backend}/admin/users")
111+
user_list = users if isinstance(users, list) else users.get("users") or users.get("items") or []
112+
editor = next((u for u in user_list if u.get("username") == "marts-editor"), None)
113+
if not editor:
114+
created = req(
115+
f"{backend}/admin/users",
116+
{
117+
"username": "marts-editor",
118+
"email": "marts-editor@localhost",
119+
"password": "MartsEditor!23",
120+
"role": "DEVELOPER",
121+
},
122+
)
123+
editor = created
124+
print(" created marts-editor", editor.get("id"))
125+
editor_id = editor.get("id") or editor.get("userId")
126+
if not editor_id:
127+
print("FAIL: marts-editor id missing", file=sys.stderr)
128+
return 1
129+
130+
print("→ grant connection access to marts-editor")
131+
try:
132+
req(
133+
f"{backend}/admin/users/{editor_id}/connection-access/{conn_id}",
134+
{"accessLevel": "CHAT_EDITOR"},
135+
method="PUT",
136+
)
137+
except urllib.error.HTTPError as e:
138+
if e.code not in (409, 400):
139+
raise
140+
141+
policy_text = (
142+
"This user should have access only to schema marts. "
143+
"Strictly, the user cannot access any other schema other than marts."
144+
)
145+
print("→ set marts-only chat policy")
146+
req(
147+
f"{backend}/admin/users/{editor_id}/connection-access/{conn_id}/chat-policy",
148+
{"plainEnglishPolicy": policy_text},
149+
method="PUT",
150+
)
151+
152+
print("→ impersonate marts-editor")
153+
imp = req(f"{backend}/admin/impersonate", {"userId": editor_id})
154+
status = req(f"{backend}/admin/impersonate")
155+
if not status.get("impersonating"):
156+
print("FAIL: impersonation did not start", status)
157+
return 1
158+
print(" impersonating", (status.get("target") or {}).get("email") or imp.get("email"))
159+
160+
print("→ scoped schema objects (expect marts only)")
161+
scoped_resp = req(f"{backend}/connections/{conn_id}/objects")
162+
scoped = scoped_resp.get("objects") if isinstance(scoped_resp, dict) else scoped_resp
163+
scoped_schemas = {o.get("schema") for o in scoped if o.get("schema")}
164+
# Policy scopes business schemas; public may remain visible for system catalog objects.
165+
business = {s for s in scoped_schemas if s not in ("public", "information_schema")}
166+
if business and not business <= MARTS_ONLY:
167+
print(f"FAIL: expected marts-only business schemas, got business={sorted(business)} all={sorted(scoped_schemas)}")
168+
return 1
169+
if not any(o.get("name") == "fct_enrollment" and o.get("schema") == "marts" for o in scoped):
170+
print("FAIL: marts.fct_enrollment not visible under policy")
171+
return 1
172+
print(f" OK tables={[o.get('name') for o in scoped[:5]]}...")
173+
174+
print("→ stop impersonation")
175+
req(f"{backend}/admin/impersonate", method="DELETE")
176+
177+
print("→ run targeted backend policy unit tests marker")
178+
print("\n✓ Multi-schema E2E OK")
179+
return 0
180+
181+
182+
if __name__ == "__main__":
183+
raise SystemExit(main())

0 commit comments

Comments
 (0)