tabulus 0.0.1__tar.gz → 0.0.2__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.
- {tabulus-0.0.1 → tabulus-0.0.2}/.github/workflows/ci.yml +9 -4
- {tabulus-0.0.1 → tabulus-0.0.2}/.mcp.json +4 -4
- {tabulus-0.0.1 → tabulus-0.0.2}/PKG-INFO +24 -22
- {tabulus-0.0.1 → tabulus-0.0.2}/README.md +21 -19
- {tabulus-0.0.1 → tabulus-0.0.2}/pyproject.toml +4 -4
- tabulus-0.0.2/src/tabulus/__init__.py +3 -0
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/cli.py +16 -16
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/config.py +4 -4
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/db.py +3 -3
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/redactor.py +3 -3
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/safety.py +1 -1
- {tabulus-0.0.1/src/vigil → tabulus-0.0.2/src/tabulus}/server.py +4 -4
- {tabulus-0.0.1 → tabulus-0.0.2}/tests/test_redactor.py +6 -6
- {tabulus-0.0.1 → tabulus-0.0.2}/tests/test_safety.py +1 -1
- tabulus-0.0.1/src/vigil/__init__.py +0 -3
- {tabulus-0.0.1 → tabulus-0.0.2}/.github/workflows/publish.yml +0 -0
- {tabulus-0.0.1 → tabulus-0.0.2}/.gitignore +0 -0
- {tabulus-0.0.1 → tabulus-0.0.2}/LICENSE +0 -0
- {tabulus-0.0.1 → tabulus-0.0.2}/tests/__init__.py +0 -0
|
@@ -56,20 +56,25 @@ jobs:
|
|
|
56
56
|
- name: Unit tests
|
|
57
57
|
run: pytest tests/ -v --tb=short
|
|
58
58
|
|
|
59
|
+
- name: Seed test table (DDL needs a separate non-readonly connection)
|
|
60
|
+
env:
|
|
61
|
+
PGPASSWORD: test
|
|
62
|
+
run: |
|
|
63
|
+
psql -h localhost -U postgres -d postgres -c "CREATE TABLE smoke (id int);"
|
|
64
|
+
|
|
59
65
|
- name: Integration smoke (live Postgres)
|
|
60
66
|
env:
|
|
61
67
|
DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
|
|
62
68
|
run: |
|
|
63
69
|
python -c "
|
|
64
70
|
import asyncio
|
|
65
|
-
from
|
|
66
|
-
from
|
|
71
|
+
from tabulus.config import load
|
|
72
|
+
from tabulus.db import get_pool, list_tables, close_pool
|
|
67
73
|
async def main():
|
|
68
74
|
pool = await get_pool(load())
|
|
69
|
-
await pool.execute('CREATE TABLE smoke (id int)')
|
|
70
75
|
tables = await list_tables(pool)
|
|
71
76
|
assert any(t['name'] == 'smoke' for t in tables), 'smoke table missing'
|
|
72
|
-
print('Integration OK')
|
|
77
|
+
print('Integration OK — found smoke table')
|
|
73
78
|
await close_pool()
|
|
74
79
|
asyncio.run(main())
|
|
75
80
|
"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"vigil": {
|
|
4
|
-
"command": ".venv/bin/
|
|
4
|
+
"command": ".venv/bin/tabulus",
|
|
5
5
|
"args": [],
|
|
6
6
|
"env": {
|
|
7
7
|
"DATABASE_URL": "postgres://postgres:dev@localhost:5433/postgres",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"TABULUS_MAX_ROWS": "100",
|
|
9
|
+
"TABULUS_SAMPLE_SIZE": "3",
|
|
10
|
+
"TABULUS_STATEMENT_TIMEOUT_MS": "5000"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabulus
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Postgres MCP server — agent-first database workbench
|
|
5
|
-
Project-URL: Repository, https://github.com/WalkingMountain/
|
|
6
|
-
Project-URL: Issues, https://github.com/WalkingMountain/
|
|
5
|
+
Project-URL: Repository, https://github.com/WalkingMountain/tabulus
|
|
6
|
+
Project-URL: Issues, https://github.com/WalkingMountain/tabulus/issues
|
|
7
7
|
Author: WalkingMountain
|
|
8
8
|
License: MIT
|
|
9
9
|
License-File: LICENSE
|
|
@@ -24,31 +24,33 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
|
24
24
|
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
|
-
#
|
|
27
|
+
# Tabulus
|
|
28
28
|
|
|
29
29
|
**A Postgres MCP server built for AI agents.**
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
Code, Cursor, or any MCP-compatible client to your Postgres database and let
|
|
33
|
-
|
|
31
|
+
Tabulus is the database workbench for the AI-augmented developer. Connect Claude
|
|
32
|
+
Code, Cursor, or any MCP-compatible client to your Postgres database and let the
|
|
33
|
+
agent introspect the schema, sample data, and write safe queries — without
|
|
34
34
|
copy-pasting schemas into chat windows.
|
|
35
35
|
|
|
36
36
|
## Why
|
|
37
37
|
|
|
38
38
|
Every modern dev workflow now includes an AI agent. Every DB GUI was designed
|
|
39
|
-
before that was true.
|
|
40
|
-
not a sidebar feature.**
|
|
39
|
+
before that was true. Tabulus flips the model: **the agent is a first-class
|
|
40
|
+
user, not a sidebar feature.**
|
|
41
41
|
|
|
42
42
|
What that means in practice:
|
|
43
43
|
|
|
44
44
|
- Schema introspection optimized for LLM context windows (compact JSON, foreign
|
|
45
45
|
keys flattened, sample rows inline).
|
|
46
46
|
- Read-only by default — `INSERT`/`UPDATE`/`DELETE`/`DDL` are rejected at the
|
|
47
|
-
gateway.
|
|
47
|
+
gateway. The agent can't drop your tables.
|
|
48
48
|
- `EXPLAIN` exposed as a tool so the agent can reason about query plans before
|
|
49
49
|
proposing optimizations.
|
|
50
50
|
- Statement timeout + row cap enforced server-side. No agent can DOS your
|
|
51
51
|
database by accident.
|
|
52
|
+
- Opt-in PII redactor (`TABULUS_REDACT=on`) scrubs emails, API keys, JWTs,
|
|
53
|
+
credit cards, phones, and IPs from tool output before the agent sees them.
|
|
52
54
|
|
|
53
55
|
## Status
|
|
54
56
|
|
|
@@ -64,10 +66,10 @@ pip install tabulus
|
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
export DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
67
|
-
|
|
69
|
+
tabulus
|
|
68
70
|
```
|
|
69
71
|
|
|
70
|
-
Then point your MCP client at the `
|
|
72
|
+
Then point your MCP client at the `tabulus` command.
|
|
71
73
|
|
|
72
74
|
### Claude Code (project-level)
|
|
73
75
|
|
|
@@ -76,8 +78,8 @@ Create `.mcp.json` in your project root:
|
|
|
76
78
|
```jsonc
|
|
77
79
|
{
|
|
78
80
|
"mcpServers": {
|
|
79
|
-
"
|
|
80
|
-
"command": "
|
|
81
|
+
"tabulus": {
|
|
82
|
+
"command": "tabulus",
|
|
81
83
|
"args": [],
|
|
82
84
|
"env": {
|
|
83
85
|
"DATABASE_URL": "postgres://user:pass@host:5432/dbname"
|
|
@@ -92,7 +94,7 @@ Restart Claude Code in that directory and approve the trust prompt.
|
|
|
92
94
|
### Claude Code (user-level via CLI)
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
|
-
claude mcp add
|
|
97
|
+
claude mcp add tabulus "$(which tabulus)" --env DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
### Cursor
|
|
@@ -102,8 +104,8 @@ Add to `~/.cursor/mcp_servers.json`:
|
|
|
102
104
|
```jsonc
|
|
103
105
|
{
|
|
104
106
|
"mcpServers": {
|
|
105
|
-
"
|
|
106
|
-
"command": "
|
|
107
|
+
"tabulus": {
|
|
108
|
+
"command": "tabulus",
|
|
107
109
|
"env": { "DATABASE_URL": "postgres://user:pass@host:5432/dbname" }
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -125,11 +127,11 @@ Add to `~/.cursor/mcp_servers.json`:
|
|
|
125
127
|
| Variable | Default | Purpose |
|
|
126
128
|
|---|---|---|
|
|
127
129
|
| `DATABASE_URL` | — (required) | Postgres connection URL |
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
130
|
+
| `TABULUS_MAX_ROWS` | `100` | Hard cap on rows returned by any tool |
|
|
131
|
+
| `TABULUS_SAMPLE_SIZE` | `3` | Sample rows included in `describe_schema` |
|
|
132
|
+
| `TABULUS_STATEMENT_TIMEOUT_MS` | `5000` | Server-side query timeout |
|
|
133
|
+
| `TABULUS_REDACT` | `off` | Set `on` to scrub PII (emails, API keys, JWTs, credit cards, phones, IPs) from `sample_rows`, `safe_select`, and `describe_schema` output before the agent sees it. Recommended for production. |
|
|
134
|
+
| `TABULUS_ALLOW_WRITES` | `false` | Set `true` to disable the write block (NOT recommended) |
|
|
133
135
|
|
|
134
136
|
## Roadmap
|
|
135
137
|
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tabulus
|
|
2
2
|
|
|
3
3
|
**A Postgres MCP server built for AI agents.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
Code, Cursor, or any MCP-compatible client to your Postgres database and let
|
|
7
|
-
|
|
5
|
+
Tabulus is the database workbench for the AI-augmented developer. Connect Claude
|
|
6
|
+
Code, Cursor, or any MCP-compatible client to your Postgres database and let the
|
|
7
|
+
agent introspect the schema, sample data, and write safe queries — without
|
|
8
8
|
copy-pasting schemas into chat windows.
|
|
9
9
|
|
|
10
10
|
## Why
|
|
11
11
|
|
|
12
12
|
Every modern dev workflow now includes an AI agent. Every DB GUI was designed
|
|
13
|
-
before that was true.
|
|
14
|
-
not a sidebar feature.**
|
|
13
|
+
before that was true. Tabulus flips the model: **the agent is a first-class
|
|
14
|
+
user, not a sidebar feature.**
|
|
15
15
|
|
|
16
16
|
What that means in practice:
|
|
17
17
|
|
|
18
18
|
- Schema introspection optimized for LLM context windows (compact JSON, foreign
|
|
19
19
|
keys flattened, sample rows inline).
|
|
20
20
|
- Read-only by default — `INSERT`/`UPDATE`/`DELETE`/`DDL` are rejected at the
|
|
21
|
-
gateway.
|
|
21
|
+
gateway. The agent can't drop your tables.
|
|
22
22
|
- `EXPLAIN` exposed as a tool so the agent can reason about query plans before
|
|
23
23
|
proposing optimizations.
|
|
24
24
|
- Statement timeout + row cap enforced server-side. No agent can DOS your
|
|
25
25
|
database by accident.
|
|
26
|
+
- Opt-in PII redactor (`TABULUS_REDACT=on`) scrubs emails, API keys, JWTs,
|
|
27
|
+
credit cards, phones, and IPs from tool output before the agent sees them.
|
|
26
28
|
|
|
27
29
|
## Status
|
|
28
30
|
|
|
@@ -38,10 +40,10 @@ pip install tabulus
|
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
export DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
41
|
-
|
|
43
|
+
tabulus
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
Then point your MCP client at the `
|
|
46
|
+
Then point your MCP client at the `tabulus` command.
|
|
45
47
|
|
|
46
48
|
### Claude Code (project-level)
|
|
47
49
|
|
|
@@ -50,8 +52,8 @@ Create `.mcp.json` in your project root:
|
|
|
50
52
|
```jsonc
|
|
51
53
|
{
|
|
52
54
|
"mcpServers": {
|
|
53
|
-
"
|
|
54
|
-
"command": "
|
|
55
|
+
"tabulus": {
|
|
56
|
+
"command": "tabulus",
|
|
55
57
|
"args": [],
|
|
56
58
|
"env": {
|
|
57
59
|
"DATABASE_URL": "postgres://user:pass@host:5432/dbname"
|
|
@@ -66,7 +68,7 @@ Restart Claude Code in that directory and approve the trust prompt.
|
|
|
66
68
|
### Claude Code (user-level via CLI)
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
|
-
claude mcp add
|
|
71
|
+
claude mcp add tabulus "$(which tabulus)" --env DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
### Cursor
|
|
@@ -76,8 +78,8 @@ Add to `~/.cursor/mcp_servers.json`:
|
|
|
76
78
|
```jsonc
|
|
77
79
|
{
|
|
78
80
|
"mcpServers": {
|
|
79
|
-
"
|
|
80
|
-
"command": "
|
|
81
|
+
"tabulus": {
|
|
82
|
+
"command": "tabulus",
|
|
81
83
|
"env": { "DATABASE_URL": "postgres://user:pass@host:5432/dbname" }
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -99,11 +101,11 @@ Add to `~/.cursor/mcp_servers.json`:
|
|
|
99
101
|
| Variable | Default | Purpose |
|
|
100
102
|
|---|---|---|
|
|
101
103
|
| `DATABASE_URL` | — (required) | Postgres connection URL |
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
104
|
+
| `TABULUS_MAX_ROWS` | `100` | Hard cap on rows returned by any tool |
|
|
105
|
+
| `TABULUS_SAMPLE_SIZE` | `3` | Sample rows included in `describe_schema` |
|
|
106
|
+
| `TABULUS_STATEMENT_TIMEOUT_MS` | `5000` | Server-side query timeout |
|
|
107
|
+
| `TABULUS_REDACT` | `off` | Set `on` to scrub PII (emails, API keys, JWTs, credit cards, phones, IPs) from `sample_rows`, `safe_select`, and `describe_schema` output before the agent sees it. Recommended for production. |
|
|
108
|
+
| `TABULUS_ALLOW_WRITES` | `false` | Set `true` to disable the write block (NOT recommended) |
|
|
107
109
|
|
|
108
110
|
## Roadmap
|
|
109
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tabulus"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
description = "Postgres MCP server — agent-first database workbench"
|
|
5
5
|
authors = [{name = "WalkingMountain"}]
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -29,11 +29,11 @@ dev = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.scripts]
|
|
32
|
-
|
|
32
|
+
tabulus = "tabulus.cli:main"
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
|
-
Repository = "https://github.com/WalkingMountain/
|
|
36
|
-
Issues = "https://github.com/WalkingMountain/
|
|
35
|
+
Repository = "https://github.com/WalkingMountain/tabulus"
|
|
36
|
+
Issues = "https://github.com/WalkingMountain/tabulus/issues"
|
|
37
37
|
|
|
38
38
|
[build-system]
|
|
39
39
|
requires = ["hatchling"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CLI entry point — `
|
|
1
|
+
"""CLI entry point — `tabulus` command.
|
|
2
2
|
|
|
3
3
|
Wraps startup errors in friendly messages so the agent / user sees actionable
|
|
4
4
|
hints instead of stack traces.
|
|
@@ -6,37 +6,37 @@ hints instead of stack traces.
|
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from tabulus import __version__
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def main() -> None:
|
|
13
13
|
if "--version" in sys.argv or "-V" in sys.argv:
|
|
14
|
-
print(f"
|
|
14
|
+
print(f"tabulus {__version__}")
|
|
15
15
|
return
|
|
16
16
|
|
|
17
17
|
if "--help" in sys.argv or "-h" in sys.argv:
|
|
18
18
|
print(
|
|
19
|
-
"
|
|
19
|
+
"tabulus — Postgres MCP server for AI agents\n"
|
|
20
20
|
"\n"
|
|
21
21
|
"Usage:\n"
|
|
22
|
-
" DATABASE_URL=postgres://user:pass@host:5432/dbname
|
|
22
|
+
" DATABASE_URL=postgres://user:pass@host:5432/dbname tabulus\n"
|
|
23
23
|
"\n"
|
|
24
24
|
"Environment variables:\n"
|
|
25
25
|
" DATABASE_URL required — Postgres connection string\n"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
26
|
+
" TABULUS_MAX_ROWS default 100 — cap on rows returned\n"
|
|
27
|
+
" TABULUS_SAMPLE_SIZE default 3 — rows in describe_schema sample\n"
|
|
28
|
+
" TABULUS_STATEMENT_TIMEOUT_MS default 5000 — server-side query timeout\n"
|
|
29
|
+
" TABULUS_REDACT default off — set 'on' to scrub PII from output\n"
|
|
30
|
+
" TABULUS_ALLOW_WRITES default false — keep false (read-only)\n"
|
|
31
31
|
"\n"
|
|
32
|
-
"Repo: https://github.com/WalkingMountain/
|
|
32
|
+
"Repo: https://github.com/WalkingMountain/tabulus"
|
|
33
33
|
)
|
|
34
34
|
return
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
37
|
# Defer import so --version/--help don't pay the asyncio + mcp cost
|
|
38
|
-
from
|
|
39
|
-
from
|
|
38
|
+
from tabulus.config import load
|
|
39
|
+
from tabulus.server import main as run_server
|
|
40
40
|
|
|
41
41
|
# Fast-fail config validation BEFORE we open the stdio MCP loop —
|
|
42
42
|
# otherwise the agent waits until first tool call to learn DATABASE_URL
|
|
@@ -45,21 +45,21 @@ def main() -> None:
|
|
|
45
45
|
run_server()
|
|
46
46
|
except RuntimeError as e:
|
|
47
47
|
# Config errors (missing DATABASE_URL, etc.) — already friendly
|
|
48
|
-
print(f"
|
|
48
|
+
print(f"tabulus: {e}", file=sys.stderr)
|
|
49
49
|
sys.exit(2)
|
|
50
50
|
except KeyboardInterrupt:
|
|
51
51
|
sys.exit(0)
|
|
52
52
|
except Exception as e:
|
|
53
53
|
# Last resort — show error class + message, hint at common causes
|
|
54
54
|
print(
|
|
55
|
-
f"
|
|
55
|
+
f"tabulus: unexpected error: {type(e).__name__}: {e}\n"
|
|
56
56
|
f"\n"
|
|
57
57
|
f"Common causes:\n"
|
|
58
58
|
f" - DATABASE_URL points at an unreachable host\n"
|
|
59
59
|
f" - Postgres requires SSL but the URL lacks ?sslmode=require\n"
|
|
60
60
|
f" - User in DATABASE_URL lacks CONNECT or USAGE privileges\n"
|
|
61
61
|
f"\n"
|
|
62
|
-
f"File an issue: https://github.com/WalkingMountain/
|
|
62
|
+
f"File an issue: https://github.com/WalkingMountain/tabulus/issues",
|
|
63
63
|
file=sys.stderr,
|
|
64
64
|
)
|
|
65
65
|
sys.exit(1)
|
|
@@ -22,8 +22,8 @@ def load() -> Config:
|
|
|
22
22
|
)
|
|
23
23
|
return Config(
|
|
24
24
|
database_url=url,
|
|
25
|
-
max_rows=int(os.environ.get("
|
|
26
|
-
sample_size=int(os.environ.get("
|
|
27
|
-
statement_timeout_ms=int(os.environ.get("
|
|
28
|
-
allow_writes=os.environ.get("
|
|
25
|
+
max_rows=int(os.environ.get("TABULUS_MAX_ROWS", "100")),
|
|
26
|
+
sample_size=int(os.environ.get("TABULUS_SAMPLE_SIZE", "3")),
|
|
27
|
+
statement_timeout_ms=int(os.environ.get("TABULUS_STATEMENT_TIMEOUT_MS", "5000")),
|
|
28
|
+
allow_writes=os.environ.get("TABULUS_ALLOW_WRITES", "false").lower() == "true",
|
|
29
29
|
)
|
|
@@ -7,8 +7,8 @@ flattened. Goal is to fit a 50-table schema into one prompt without truncation.
|
|
|
7
7
|
import asyncpg
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from tabulus.config import Config
|
|
11
|
+
from tabulus.redactor import maybe_redact
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
_pool: asyncpg.Pool | None = None
|
|
@@ -169,7 +169,7 @@ async def safe_select(
|
|
|
169
169
|
max_rows: int,
|
|
170
170
|
) -> dict[str, Any]:
|
|
171
171
|
# Wrap with LIMIT enforcement (subquery prevents user-supplied LIMIT bypass)
|
|
172
|
-
wrapped = f"SELECT * FROM ({sql})
|
|
172
|
+
wrapped = f"SELECT * FROM ({sql}) _tabulus_q LIMIT {int(max_rows)}"
|
|
173
173
|
rows = await pool.fetch(wrapped)
|
|
174
174
|
return {
|
|
175
175
|
"row_count": len(rows),
|
|
@@ -11,7 +11,7 @@ leaking the value.
|
|
|
11
11
|
|
|
12
12
|
Conservative philosophy: false positives are cheap, false negatives kill.
|
|
13
13
|
|
|
14
|
-
Off by default — set
|
|
14
|
+
Off by default — set TABULUS_REDACT=on to enable.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
@@ -65,7 +65,7 @@ _PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def is_enabled() -> bool:
|
|
68
|
-
return os.environ.get("
|
|
68
|
+
return os.environ.get("TABULUS_REDACT", "off").lower() in ("on", "true", "1", "yes")
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def redact_string(s: str) -> str:
|
|
@@ -92,5 +92,5 @@ def redact_value(v: Any) -> Any:
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def maybe_redact(v: Any) -> Any:
|
|
95
|
-
"""No-op when
|
|
95
|
+
"""No-op when TABULUS_REDACT is off, redact otherwise."""
|
|
96
96
|
return redact_value(v) if is_enabled() else v
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""SQL safety — read-only enforcement.
|
|
2
2
|
|
|
3
3
|
Rejects any statement that could mutate data or schema. Default mode for the
|
|
4
|
-
agent: SELECT + EXPLAIN only. Writes only enabled when
|
|
4
|
+
agent: SELECT + EXPLAIN only. Writes only enabled when TABULUS_ALLOW_WRITES=true
|
|
5
5
|
AND the human operator opts in per-statement (future approval flow).
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -18,8 +18,8 @@ from mcp.server import Server
|
|
|
18
18
|
from mcp.server.stdio import stdio_server
|
|
19
19
|
from mcp.types import TextContent, Tool
|
|
20
20
|
|
|
21
|
-
from
|
|
22
|
-
from
|
|
21
|
+
from tabulus.config import load
|
|
22
|
+
from tabulus.db import (
|
|
23
23
|
close_pool,
|
|
24
24
|
describe_table,
|
|
25
25
|
explain,
|
|
@@ -28,10 +28,10 @@ from vigil.db import (
|
|
|
28
28
|
safe_select,
|
|
29
29
|
sample_rows,
|
|
30
30
|
)
|
|
31
|
-
from
|
|
31
|
+
from tabulus.safety import UnsafeSQLError, assert_read_only
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
server = Server("
|
|
34
|
+
server = Server("tabulus")
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@server.list_tools()
|
|
@@ -5,7 +5,7 @@ from unittest.mock import patch
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from tabulus.redactor import (
|
|
9
9
|
is_enabled,
|
|
10
10
|
maybe_redact,
|
|
11
11
|
redact_string,
|
|
@@ -121,18 +121,18 @@ def test_disabled_by_default():
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def test_enabled_via_env():
|
|
124
|
-
with patch.dict(os.environ, {"
|
|
124
|
+
with patch.dict(os.environ, {"TABULUS_REDACT": "on"}):
|
|
125
125
|
assert is_enabled() is True
|
|
126
126
|
assert maybe_redact("foo@bar.com") == "[REDACTED:email]"
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def test_enabled_alt_values():
|
|
130
130
|
for val in ("on", "ON", "true", "True", "1", "yes"):
|
|
131
|
-
with patch.dict(os.environ, {"
|
|
132
|
-
assert is_enabled() is True, f"
|
|
131
|
+
with patch.dict(os.environ, {"TABULUS_REDACT": val}):
|
|
132
|
+
assert is_enabled() is True, f"TABULUS_REDACT={val!r} should enable"
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
def test_disabled_alt_values():
|
|
136
136
|
for val in ("off", "false", "0", "no", "", "anything-else"):
|
|
137
|
-
with patch.dict(os.environ, {"
|
|
138
|
-
assert is_enabled() is False, f"
|
|
137
|
+
with patch.dict(os.environ, {"TABULUS_REDACT": val}):
|
|
138
|
+
assert is_enabled() is False, f"TABULUS_REDACT={val!r} should NOT enable"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|