postgres-aiops 0.3.0__tar.gz → 0.5.0__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.
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/CHANGELOG.md +11 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/PKG-INFO +55 -9
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/README.md +53 -7
- postgres_aiops-0.5.0/RELEASE_NOTES.md +50 -0
- postgres_aiops-0.5.0/docs/VERIFICATION.md +159 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/_shared.py +27 -5
- postgres_aiops-0.5.0/mcp_server/server.py +76 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/analysis.py +18 -4
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/remediation.py +18 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/undo.py +24 -2
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/_common.py +49 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/analyze.py +22 -8
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/index.py +13 -3
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/query.py +2 -1
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/remediate.py +23 -12
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/table.py +22 -8
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/connection.py +23 -1
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/__init__.py +15 -1
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/decorators.py +77 -9
- postgres_aiops-0.5.0/postgres_aiops/governance/outcome.py +104 -0
- postgres_aiops-0.5.0/postgres_aiops/governance/readonly.py +49 -0
- postgres_aiops-0.5.0/postgres_aiops/governance/sanitize.py +80 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/undo.py +52 -13
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/_util.py +23 -2
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/activity.py +28 -28
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/analysis.py +6 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/indexes.py +22 -4
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/queries.py +20 -5
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/remediation.py +122 -10
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/replication.py +15 -15
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/server.py +8 -8
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/tables.py +69 -21
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/pyproject.toml +2 -2
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/server.json +3 -3
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/skills/postgres-aiops/SKILL.md +45 -19
- postgres_aiops-0.5.0/skills/postgres-aiops/references/agent-guardrails.md +111 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/skills/postgres-aiops/references/capabilities.md +6 -4
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/skills/postgres-aiops/references/cli-reference.md +28 -8
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/skills/postgres-aiops/references/setup-guide.md +1 -1
- postgres_aiops-0.5.0/tests/test_cli_reads.py +257 -0
- postgres_aiops-0.5.0/tests/test_cli_remediate.py +134 -0
- postgres_aiops-0.5.0/tests/test_cli_secret.py +98 -0
- postgres_aiops-0.5.0/tests/test_config.py +115 -0
- postgres_aiops-0.5.0/tests/test_connection_lost.py +69 -0
- postgres_aiops-0.5.0/tests/test_connection_more.py +171 -0
- postgres_aiops-0.5.0/tests/test_gov_audit.py +292 -0
- postgres_aiops-0.5.0/tests/test_gov_decorators.py +452 -0
- postgres_aiops-0.5.0/tests/test_gov_patterns.py +417 -0
- postgres_aiops-0.5.0/tests/test_gov_policy.py +415 -0
- postgres_aiops-0.5.0/tests/test_governance_persistence.py +362 -0
- postgres_aiops-0.5.0/tests/test_mcp_tools.py +224 -0
- postgres_aiops-0.5.0/tests/test_ops_more.py +135 -0
- postgres_aiops-0.5.0/tests/test_optional_fields.py +200 -0
- postgres_aiops-0.5.0/tests/test_readonly.py +145 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_reads.py +3 -1
- postgres_aiops-0.5.0/tests/test_secretstore_more.py +140 -0
- postgres_aiops-0.5.0/tests/test_self_lockout.py +195 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_smoke.py +5 -2
- postgres_aiops-0.5.0/tests/test_truncation.py +162 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_undo_executor.py +1 -1
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/uv.lock +1 -1
- postgres_aiops-0.3.0/RELEASE_NOTES.md +0 -52
- postgres_aiops-0.3.0/mcp_server/server.py +0 -38
- postgres_aiops-0.3.0/postgres_aiops/governance/sanitize.py +0 -45
- postgres_aiops-0.3.0/tests/test_governance_persistence.py +0 -170
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/.github/workflows/mcp-publish.yml +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/.github/workflows/publish.yml +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/.gitignore +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/LICENSE +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/SECURITY.md +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/__init__.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/__init__.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/activity.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/indexes.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/queries.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/replication.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/server.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/mcp_server/tools/tables.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/__init__.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/__init__.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/_root.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/activity.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/doctor.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/init.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/overview.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/replication.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/secret.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/server.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/cli/undo.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/config.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/doctor.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/audit.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/budget.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/paths.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/patterns.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/governance/policy.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/__init__.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/ops/overview.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/postgres_aiops/secretstore.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/smithery.yaml +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/conftest.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_analysis.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_cli_writes.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_connection.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_doctor.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_init.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_secretstore.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_undo_replay.py +0 -0
- {postgres_aiops-0.3.0 → postgres_aiops-0.5.0}/tests/test_writes.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.5.0 — 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`terminate_backend` / `cancel_query` refuse this tool's own backend**, and `update_setting` refuses the postmaster settings that would strand the undo at the next restart (`listen_addresses`, `port`, `max_connections`, `ssl`, `hba_file`, …).
|
|
7
|
+
- A connection lost **mid-statement** now raises a dedicated error and is audited as `unknown`, not `error`..
|
|
8
|
+
- Harness: a write whose response is lost is audited `status=unknown`, not `error` — it may have taken effect. Undo tokens gain `effectVerified` (undo.db migrated in place).
|
|
9
|
+
- Harness: a dry-run no longer records an undo token, and no longer requires a named approver. Guards now run on the preview path.
|
|
10
|
+
- Truncated strings end in an ellipsis instead of being cut silently; error messages are capped at 800 chars, not 300.
|
|
11
|
+
|
|
12
|
+
See RELEASE_NOTES.md for the full detail.
|
|
13
|
+
|
|
3
14
|
## v0.3.0 — 2026-07-17
|
|
4
15
|
|
|
5
16
|
### Added
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: postgres-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Governed AI-ops for PostgreSQL DBA operations: slow-query RCA, bloat/vacuum & blocking-lock analysis with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Wei Zhou <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
<!-- mcp-name: io.github.AIops-tools/postgres-aiops -->
|
|
18
18
|
|
|
19
|
-
# Postgres AIops
|
|
19
|
+
# Postgres AIops
|
|
20
20
|
|
|
21
21
|
> **Disclaimer**: Community-maintained open-source project. **Not affiliated with, endorsed by, or sponsored by the PostgreSQL Global Development Group or any vendor.** "PostgreSQL" and the elephant logo are trademarks of the PostgreSQL Community Association; all product/trademark names belong to their respective owners. MIT licensed.
|
|
22
22
|
|
|
@@ -24,7 +24,8 @@ Governed AI-ops for **PostgreSQL DBA operations** — connecting to a server wit
|
|
|
24
24
|
**psycopg 3** and reading the system catalogs and `pg_stat_*` views — with a
|
|
25
25
|
**built-in governance harness**: unified audit log, policy engine, token/runaway
|
|
26
26
|
budget guard, undo-token recording, and graduated-autonomy risk tiers.
|
|
27
|
-
|
|
27
|
+
Beyond the mock test suite, the reads, a governed write, and its undo have been
|
|
28
|
+
exercised against a live PostgreSQL 16.14 instance — see [`docs/VERIFICATION.md`](docs/VERIFICATION.md).
|
|
28
29
|
|
|
29
30
|
## What it does
|
|
30
31
|
|
|
@@ -43,12 +44,12 @@ Three flagship signature analyses, plus the guarded reads and writes around them
|
|
|
43
44
|
## What works
|
|
44
45
|
|
|
45
46
|
- **CLI** (`postgres-aiops ...`): `init`, `overview`, `server`, `activity`, `query`, `index`, `table`, `repl`, `analyze`, `remediate`, `secret`, `doctor`, `mcp`.
|
|
46
|
-
- **MCP server** (`postgres-aiops mcp` or `postgres-aiops-mcp`): **
|
|
47
|
+
- **MCP server** (`postgres-aiops mcp` or `postgres-aiops-mcp`): **35 tools** (25 read, 10 write), every one wrapped with the bundled `@governed_tool` harness.
|
|
47
48
|
- **Encrypted credentials**: the role password lives in an encrypted store `~/.postgres-aiops/secrets.enc` (Fernet + scrypt) — **never plaintext on disk**. Unlock with a master password from `POSTGRES_AIOPS_MASTER_PASSWORD` (MCP/CI) or an interactive prompt (CLI).
|
|
48
49
|
- **Reversibility**: mutating writes fetch the **real before-state first** and record a faithful inverse — `create_index`↔`drop_index`; `drop_index` captures `pg_get_indexdef` so undo recreates it exactly; `update_setting` captures the prior value so undo sets it back. Irreversible ops (`terminate_backend`, `cancel_query`, `run_vacuum`, `run_analyze`, `reindex`, `reset_query_stats`) record prior stats for audit but declare no undo.
|
|
49
50
|
- **Safety**: every state-changing CLI op supports `--dry-run` and requires double confirmation; every write MCP tool takes a `dry_run` preview. All identifiers that cannot be parameterised (table/index/GUC names) are validated and quoted; all values are bound query parameters.
|
|
50
51
|
|
|
51
|
-
## Capability matrix (
|
|
52
|
+
## Capability matrix (35 MCP tools)
|
|
52
53
|
|
|
53
54
|
| Domain | Tools | Count | R/W |
|
|
54
55
|
|--------|-------|:-----:|:---:|
|
|
@@ -62,11 +63,53 @@ Three flagship signature analyses, plus the guarded reads and writes around them
|
|
|
62
63
|
| **Analysis (flagship)** | `slow_query_rca`, `bloat_and_vacuum_analysis`, `blocking_lock_chain_rca` | 3 | read |
|
|
63
64
|
| **Writes** | `terminate_backend`, `cancel_query`, `drop_index` | 3 | write (high) |
|
|
64
65
|
| | `run_vacuum`, `run_analyze`, `create_index`, `reindex`, `update_setting`, `reset_query_stats` | 6 | write (medium) |
|
|
66
|
+
| **Undo** | `undo_list` | 1 | read |
|
|
67
|
+
| | `undo_apply` | 1 | write (medium) |
|
|
65
68
|
|
|
66
69
|
The flagship analyses accept injected records for pure/offline analysis, or pull
|
|
67
70
|
live from a configured target. `top_queries`/`slow_query_rca` require the
|
|
68
71
|
`pg_stat_statements` extension; the read role should have `pg_monitor`.
|
|
69
72
|
|
|
73
|
+
## Security: read-only mode
|
|
74
|
+
|
|
75
|
+
This tool is meant to be handed to an AI agent, so its safety story is enforced
|
|
76
|
+
by the server rather than requested in a prompt:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export POSTGRES_READ_ONLY=1
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With that set, the **10 write tools are never registered**. An MCP client
|
|
83
|
+
lists **25 tools instead of 35** — the writes are not hidden, not
|
|
84
|
+
gated behind a flag, and not merely refused when called. They are absent from
|
|
85
|
+
the session. A model cannot invoke a tool it was never offered, and cannot be
|
|
86
|
+
argued into one.
|
|
87
|
+
|
|
88
|
+
That distinction is the whole point. A tool that exists but refuses still invites
|
|
89
|
+
retry loops and "I'll describe the call instead" behaviour from smaller models,
|
|
90
|
+
and it leaves a reviewer trusting a promise. An absent tool is a fact you can
|
|
91
|
+
check: connect, list the tools, and see that the writes are not there.
|
|
92
|
+
|
|
93
|
+
Enforcement is two layers deep, so the switch cannot be sidestepped by changing
|
|
94
|
+
entry point:
|
|
95
|
+
|
|
96
|
+
| Layer | What it does | Covers |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
|
|
99
|
+
| MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
|
|
100
|
+
|
|
101
|
+
Read operations are unaffected, and every call is still audited to
|
|
102
|
+
`~/.postgres-aiops/audit.db`.
|
|
103
|
+
|
|
104
|
+
> The read/write split is derived from each tool's declared `risk_level`, and a
|
|
105
|
+
> test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
|
|
106
|
+
> tool's own documentation — so a write can't quietly present itself as a read.
|
|
107
|
+
|
|
108
|
+
Running a smaller / local model? See
|
|
109
|
+
[agent-guardrails.md](skills/postgres-aiops/references/agent-guardrails.md) — it lists
|
|
110
|
+
the guardrails this tool now enforces for you (so you don't spend prompt budget
|
|
111
|
+
restating them) and gives a ready-made system prompt for what's left.
|
|
112
|
+
|
|
70
113
|
## Quick start
|
|
71
114
|
|
|
72
115
|
```bash
|
|
@@ -114,6 +157,9 @@ an issue or PR** — contributions welcome.
|
|
|
114
157
|
|
|
115
158
|
## Status
|
|
116
159
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
The mock test suite is complemented by a live run: the catalog / `pg_stat_*`
|
|
161
|
+
reads, the `bloat_and_vacuum_analysis` RCA, and the `create_index` / `drop_index`
|
|
162
|
+
governed write path (audit + undo, with `drop_index` capturing
|
|
163
|
+
`pg_get_indexdef` first) were exercised against a live PostgreSQL 16.14 instance running in
|
|
164
|
+
Docker. [`docs/VERIFICATION.md`](docs/VERIFICATION.md) records exactly what was
|
|
165
|
+
and was not covered. `postgres-aiops doctor` is the fastest live check.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!-- mcp-name: io.github.AIops-tools/postgres-aiops -->
|
|
2
2
|
|
|
3
|
-
# Postgres AIops
|
|
3
|
+
# Postgres AIops
|
|
4
4
|
|
|
5
5
|
> **Disclaimer**: Community-maintained open-source project. **Not affiliated with, endorsed by, or sponsored by the PostgreSQL Global Development Group or any vendor.** "PostgreSQL" and the elephant logo are trademarks of the PostgreSQL Community Association; all product/trademark names belong to their respective owners. MIT licensed.
|
|
6
6
|
|
|
@@ -8,7 +8,8 @@ Governed AI-ops for **PostgreSQL DBA operations** — connecting to a server wit
|
|
|
8
8
|
**psycopg 3** and reading the system catalogs and `pg_stat_*` views — with a
|
|
9
9
|
**built-in governance harness**: unified audit log, policy engine, token/runaway
|
|
10
10
|
budget guard, undo-token recording, and graduated-autonomy risk tiers.
|
|
11
|
-
|
|
11
|
+
Beyond the mock test suite, the reads, a governed write, and its undo have been
|
|
12
|
+
exercised against a live PostgreSQL 16.14 instance — see [`docs/VERIFICATION.md`](docs/VERIFICATION.md).
|
|
12
13
|
|
|
13
14
|
## What it does
|
|
14
15
|
|
|
@@ -27,12 +28,12 @@ Three flagship signature analyses, plus the guarded reads and writes around them
|
|
|
27
28
|
## What works
|
|
28
29
|
|
|
29
30
|
- **CLI** (`postgres-aiops ...`): `init`, `overview`, `server`, `activity`, `query`, `index`, `table`, `repl`, `analyze`, `remediate`, `secret`, `doctor`, `mcp`.
|
|
30
|
-
- **MCP server** (`postgres-aiops mcp` or `postgres-aiops-mcp`): **
|
|
31
|
+
- **MCP server** (`postgres-aiops mcp` or `postgres-aiops-mcp`): **35 tools** (25 read, 10 write), every one wrapped with the bundled `@governed_tool` harness.
|
|
31
32
|
- **Encrypted credentials**: the role password lives in an encrypted store `~/.postgres-aiops/secrets.enc` (Fernet + scrypt) — **never plaintext on disk**. Unlock with a master password from `POSTGRES_AIOPS_MASTER_PASSWORD` (MCP/CI) or an interactive prompt (CLI).
|
|
32
33
|
- **Reversibility**: mutating writes fetch the **real before-state first** and record a faithful inverse — `create_index`↔`drop_index`; `drop_index` captures `pg_get_indexdef` so undo recreates it exactly; `update_setting` captures the prior value so undo sets it back. Irreversible ops (`terminate_backend`, `cancel_query`, `run_vacuum`, `run_analyze`, `reindex`, `reset_query_stats`) record prior stats for audit but declare no undo.
|
|
33
34
|
- **Safety**: every state-changing CLI op supports `--dry-run` and requires double confirmation; every write MCP tool takes a `dry_run` preview. All identifiers that cannot be parameterised (table/index/GUC names) are validated and quoted; all values are bound query parameters.
|
|
34
35
|
|
|
35
|
-
## Capability matrix (
|
|
36
|
+
## Capability matrix (35 MCP tools)
|
|
36
37
|
|
|
37
38
|
| Domain | Tools | Count | R/W |
|
|
38
39
|
|--------|-------|:-----:|:---:|
|
|
@@ -46,11 +47,53 @@ Three flagship signature analyses, plus the guarded reads and writes around them
|
|
|
46
47
|
| **Analysis (flagship)** | `slow_query_rca`, `bloat_and_vacuum_analysis`, `blocking_lock_chain_rca` | 3 | read |
|
|
47
48
|
| **Writes** | `terminate_backend`, `cancel_query`, `drop_index` | 3 | write (high) |
|
|
48
49
|
| | `run_vacuum`, `run_analyze`, `create_index`, `reindex`, `update_setting`, `reset_query_stats` | 6 | write (medium) |
|
|
50
|
+
| **Undo** | `undo_list` | 1 | read |
|
|
51
|
+
| | `undo_apply` | 1 | write (medium) |
|
|
49
52
|
|
|
50
53
|
The flagship analyses accept injected records for pure/offline analysis, or pull
|
|
51
54
|
live from a configured target. `top_queries`/`slow_query_rca` require the
|
|
52
55
|
`pg_stat_statements` extension; the read role should have `pg_monitor`.
|
|
53
56
|
|
|
57
|
+
## Security: read-only mode
|
|
58
|
+
|
|
59
|
+
This tool is meant to be handed to an AI agent, so its safety story is enforced
|
|
60
|
+
by the server rather than requested in a prompt:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export POSTGRES_READ_ONLY=1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With that set, the **10 write tools are never registered**. An MCP client
|
|
67
|
+
lists **25 tools instead of 35** — the writes are not hidden, not
|
|
68
|
+
gated behind a flag, and not merely refused when called. They are absent from
|
|
69
|
+
the session. A model cannot invoke a tool it was never offered, and cannot be
|
|
70
|
+
argued into one.
|
|
71
|
+
|
|
72
|
+
That distinction is the whole point. A tool that exists but refuses still invites
|
|
73
|
+
retry loops and "I'll describe the call instead" behaviour from smaller models,
|
|
74
|
+
and it leaves a reviewer trusting a promise. An absent tool is a fact you can
|
|
75
|
+
check: connect, list the tools, and see that the writes are not there.
|
|
76
|
+
|
|
77
|
+
Enforcement is two layers deep, so the switch cannot be sidestepped by changing
|
|
78
|
+
entry point:
|
|
79
|
+
|
|
80
|
+
| Layer | What it does | Covers |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
|
|
83
|
+
| MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
|
|
84
|
+
|
|
85
|
+
Read operations are unaffected, and every call is still audited to
|
|
86
|
+
`~/.postgres-aiops/audit.db`.
|
|
87
|
+
|
|
88
|
+
> The read/write split is derived from each tool's declared `risk_level`, and a
|
|
89
|
+
> test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
|
|
90
|
+
> tool's own documentation — so a write can't quietly present itself as a read.
|
|
91
|
+
|
|
92
|
+
Running a smaller / local model? See
|
|
93
|
+
[agent-guardrails.md](skills/postgres-aiops/references/agent-guardrails.md) — it lists
|
|
94
|
+
the guardrails this tool now enforces for you (so you don't spend prompt budget
|
|
95
|
+
restating them) and gives a ready-made system prompt for what's left.
|
|
96
|
+
|
|
54
97
|
## Quick start
|
|
55
98
|
|
|
56
99
|
```bash
|
|
@@ -98,6 +141,9 @@ an issue or PR** — contributions welcome.
|
|
|
98
141
|
|
|
99
142
|
## Status
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
The mock test suite is complemented by a live run: the catalog / `pg_stat_*`
|
|
145
|
+
reads, the `bloat_and_vacuum_analysis` RCA, and the `create_index` / `drop_index`
|
|
146
|
+
governed write path (audit + undo, with `drop_index` capturing
|
|
147
|
+
`pg_get_indexdef` first) were exercised against a live PostgreSQL 16.14 instance running in
|
|
148
|
+
Docker. [`docs/VERIFICATION.md`](docs/VERIFICATION.md) records exactly what was
|
|
149
|
+
and was not covered. `postgres-aiops doctor` is the fastest live check.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Release notes — postgres-aiops 0.5.0
|
|
2
|
+
|
|
3
|
+
Previous release: 0.4.0.
|
|
4
|
+
|
|
5
|
+
## In this tool
|
|
6
|
+
|
|
7
|
+
- **`terminate_backend` / `cancel_query` refuse this tool's own backend**, and `update_setting` refuses the postmaster settings that would strand the undo at the next restart (`listen_addresses`, `port`, `max_connections`, `ssl`, `hba_file`, …). The read path already filtered `pg_backend_pid()`; the writes did not.
|
|
8
|
+
- A connection lost **mid-statement** now raises a dedicated error and is audited as `unknown`, not `error`.
|
|
9
|
+
|
|
10
|
+
## Every tool in the line: previews and undetermined outcomes
|
|
11
|
+
|
|
12
|
+
This release fixes three harness defects that were silently degrading the audit
|
|
13
|
+
trail and the undo store.
|
|
14
|
+
|
|
15
|
+
**A write that loses its response is no longer recorded as a failure.** The
|
|
16
|
+
harness assumed a sanitized error meant nothing had happened. That assumption is
|
|
17
|
+
false in exactly the case that matters most: when a write severs its own
|
|
18
|
+
connection, the request has already landed, the response cannot come back, and
|
|
19
|
+
the operation was recorded as `status=error` with **no undo token created at
|
|
20
|
+
all**. Transport-level failures are now audited as `status=unknown`, the result
|
|
21
|
+
says plainly that the operation may have taken effect and should be verified
|
|
22
|
+
before retrying, and a write that stashed its before-state has its inverse
|
|
23
|
+
recorded anyway — flagged `effectVerified: false`, which `undo_list` and
|
|
24
|
+
`undo_apply` both surface. Existing `undo.db` files are migrated in place; their
|
|
25
|
+
rows read as verified, which is accurate, since the old code only ever recorded
|
|
26
|
+
on the confirmed path.
|
|
27
|
+
|
|
28
|
+
**A dry-run no longer writes an undo token.** Previews were recording inverses
|
|
29
|
+
built from a before-state they never had: the undo callback's permissive default
|
|
30
|
+
filled the gap with a guess, producing a real, applicable token for an operation
|
|
31
|
+
that never happened.
|
|
32
|
+
|
|
33
|
+
**A dry-run no longer demands a named approver.** Requiring an approval in order
|
|
34
|
+
to ask whether something needs approval inverts what a preview is for. The tier
|
|
35
|
+
is still computed and still audited, so the preview can tell you an approver
|
|
36
|
+
will be needed; it just no longer refuses to answer. The write itself is gated
|
|
37
|
+
exactly as before.
|
|
38
|
+
|
|
39
|
+
The invariant, now stated: **a dry_run may read; it must never write.** Guards
|
|
40
|
+
run on the preview path, which means a preview can and does report that an
|
|
41
|
+
operation would be refused.
|
|
42
|
+
|
|
43
|
+
## Also line-wide
|
|
44
|
+
|
|
45
|
+
- **Truncated text now ends in an ellipsis** instead of being cut silently. This
|
|
46
|
+
line already treats a silent cut as a defect for lists; it was doing exactly
|
|
47
|
+
that to strings.
|
|
48
|
+
- **Error messages are capped at 800 characters, not 300.** These messages end
|
|
49
|
+
with what to do instead, so the cap was removing the most useful sentence of
|
|
50
|
+
every long refusal.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Live verification
|
|
2
|
+
|
|
3
|
+
`postgres-aiops` has been **exercised against a live PostgreSQL instance**
|
|
4
|
+
(PostgreSQL **16.14**, running in Docker) in addition to its mock test suite.
|
|
5
|
+
This document records exactly what that run covered, what it did **not**, and the
|
|
6
|
+
checklist any further live run should follow. It is deliberately checklist-shaped
|
|
7
|
+
so the result is reproducible and auditable — not a subjective "seems fine".
|
|
8
|
+
|
|
9
|
+
**Scope of the claim**: the catalog / `pg_stat_*` reads, the
|
|
10
|
+
`bloat_and_vacuum_analysis` RCA, and the `create_index` / `drop_index` governed
|
|
11
|
+
write path (audit + undo) were run against that instance and behaved as the mock
|
|
12
|
+
suite predicts. Sections left open below are **not** claimed.
|
|
13
|
+
|
|
14
|
+
## What the mock suite already guarantees
|
|
15
|
+
|
|
16
|
+
- Every module imports; the CLI builds; every MCP tool carries the
|
|
17
|
+
`@governed_tool` harness marker (`tests/test_smoke.py`).
|
|
18
|
+
- The three flagship analyses are unit-tested against synthetic rows:
|
|
19
|
+
`slow_query_rca` (seq scan / cache-hit / temp-spill / call-count findings, each
|
|
20
|
+
citing its measured number), `bloat_and_vacuum_analysis` (dead-tuple ratio +
|
|
21
|
+
autovacuum lag ranking), and `blocking_lock_chain_rca` (wait-for tree with the
|
|
22
|
+
root blocker named, not just the visible victims).
|
|
23
|
+
- SQL safety: all values are bound query parameters; identifiers that cannot be
|
|
24
|
+
parameterised (table / index / GUC names, `ORDER BY` columns, index methods)
|
|
25
|
+
are validated against strict allow-lists and quoted; `EXPLAIN` rejects
|
|
26
|
+
multi-statement input.
|
|
27
|
+
- Reversible writes record the correct inverse: `create_index` ↔ `drop_index`
|
|
28
|
+
(with `drop_index` capturing `pg_get_indexdef` **before** dropping), and
|
|
29
|
+
`update_setting` capturing the prior value. Irreversible ops (terminate,
|
|
30
|
+
cancel, vacuum, analyze, reindex, reset stats) declare no undo.
|
|
31
|
+
- Governance persistence: audited rows land in the SQLite audit DB, and the
|
|
32
|
+
secure-by-default approver gate refuses high-risk writes with no `rules.yaml`
|
|
33
|
+
and no `POSTGRES_AUDIT_APPROVED_BY`.
|
|
34
|
+
|
|
35
|
+
## Prerequisites for a live run
|
|
36
|
+
|
|
37
|
+
A reachable PostgreSQL server you are willing to write to — a Docker container
|
|
38
|
+
is enough, and is what the recorded run used:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docker run -d --name pg-verify -e POSTGRES_PASSWORD=... -p 5432:5432 postgres:16
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- A role with `pg_monitor` for the reads; `pg_stat_statements` must be
|
|
45
|
+
**installed and loaded** (`shared_preload_libraries`) for `top_queries` and
|
|
46
|
+
`slow_query_rca`.
|
|
47
|
+
- A **throwaway database and table** you are willing to vacuum, index, and drop
|
|
48
|
+
indexes on. Never verify against production.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv tool install postgres-aiops
|
|
52
|
+
postgres-aiops init # encrypted secret store for the role password
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Verification checklist
|
|
56
|
+
|
|
57
|
+
Boxes marked ✅ were confirmed on **PostgreSQL 16.14 (Docker)**. Unticked boxes
|
|
58
|
+
are open — record them as gaps rather than silently passing.
|
|
59
|
+
|
|
60
|
+
### 1. Connectivity (the fastest live gate)
|
|
61
|
+
- [x] ✅ `postgres-aiops doctor` → green (config, encrypted secret store, and a
|
|
62
|
+
real connection + `server_version` against the instance).
|
|
63
|
+
|
|
64
|
+
### 2. Reads return real, well-shaped data
|
|
65
|
+
- [x] ✅ `postgres-aiops overview` → real connection counts, database sizes, and
|
|
66
|
+
longest-query figures matching the instance.
|
|
67
|
+
- [x] ✅ `postgres-aiops server version` / `server settings` / `server extensions`
|
|
68
|
+
/ `server databases` / `server roles` → real catalog contents.
|
|
69
|
+
- [x] ✅ `postgres-aiops activity list` and `activity long --min-seconds 1` →
|
|
70
|
+
real `pg_stat_activity` rows; no crash on NULL query text or missing
|
|
71
|
+
fields.
|
|
72
|
+
- [x] ✅ `postgres-aiops table sizes` / `table bloat` / `table autovacuum` →
|
|
73
|
+
values agree with a hand query against `pg_stat_user_tables`.
|
|
74
|
+
- [x] ✅ `postgres-aiops index unused` / `index missing` / `index bloat` /
|
|
75
|
+
`index invalid` → real index rows, correct emptiness on a fresh database.
|
|
76
|
+
- [ ] `postgres-aiops query top` / `query explain "<sql>"` against an instance
|
|
77
|
+
with `pg_stat_statements` actually loaded (the recorded run did not have
|
|
78
|
+
the extension preloaded — **open gap**).
|
|
79
|
+
- [ ] `postgres-aiops repl status` / `repl slots` / `repl wal` against a real
|
|
80
|
+
primary/standby pair (the recorded run was a single node — **open gap**).
|
|
81
|
+
|
|
82
|
+
### 3. The flagship analyses hold up against real telemetry
|
|
83
|
+
- [x] ✅ `postgres-aiops analyze bloat-vacuum` → on a table deliberately loaded
|
|
84
|
+
and then bulk-deleted, the RCA correctly ranked the table with ~50% dead
|
|
85
|
+
tuples first and cited the measured ratio.
|
|
86
|
+
- [x] ✅ `postgres-aiops analyze blocking` → returns cleanly (empty chain) with no
|
|
87
|
+
contention present.
|
|
88
|
+
- [ ] `analyze blocking` against a **real** blocking pile-up: open a transaction
|
|
89
|
+
that holds a row lock, block a second session on it, and confirm the
|
|
90
|
+
wait-for tree names the first session as root blocker (**open gap** —
|
|
91
|
+
only the empty case was exercised live).
|
|
92
|
+
- [ ] `postgres-aiops analyze slow-query` against real `pg_stat_statements` data
|
|
93
|
+
(blocked on the same extension gap as section 2).
|
|
94
|
+
|
|
95
|
+
### 4. A reversible write + its undo (governance closes the loop)
|
|
96
|
+
- [x] ✅ `postgres-aiops remediate create-index <table> <col> --dry-run` → printed
|
|
97
|
+
the exact DDL, changed nothing.
|
|
98
|
+
- [x] ✅ `create_index` for real → the index appeared in `pg_indexes`; the result
|
|
99
|
+
carried an `_undo_id`; a row landed in `~/.postgres-aiops/audit.db`.
|
|
100
|
+
- [x] ✅ `drop_index` for real → the index was gone, and the undo descriptor held
|
|
101
|
+
the **captured** `pg_get_indexdef` definition, not a reconstruction.
|
|
102
|
+
- [x] ✅ `postgres-aiops undo apply <id>` → replayed correctly and recreated the
|
|
103
|
+
index from the captured definition. (A replay bug found in an earlier
|
|
104
|
+
round was fixed and is now covered by a regression test.)
|
|
105
|
+
- [ ] `remediate set <guc> <value>` then `undo apply` → the prior value restored
|
|
106
|
+
(**open gap** — `update_setting` was not exercised live).
|
|
107
|
+
|
|
108
|
+
### 5. Irreversible writes are honest about it
|
|
109
|
+
- [x] ✅ `remediate vacuum <table> --analyze` → the dead tuples were actually
|
|
110
|
+
reclaimed (confirmed by re-running `analyze bloat-vacuum`); the audit row
|
|
111
|
+
records prior stats and declares **no** undo.
|
|
112
|
+
- [ ] `remediate terminate <pid>` / `remediate cancel <pid>` against a real
|
|
113
|
+
long-running backend (**open gap** — not exercised live).
|
|
114
|
+
- [ ] `remediate reindex` on a real index (**open gap**).
|
|
115
|
+
|
|
116
|
+
### 6. Governance actually gates
|
|
117
|
+
- [x] ✅ With no `~/.postgres-aiops/rules.yaml`, a high-risk write was refused
|
|
118
|
+
until `POSTGRES_AUDIT_APPROVED_BY` named an approver (secure-by-default);
|
|
119
|
+
the approver and `POSTGRES_AUDIT_RATIONALE` appear in the audit row.
|
|
120
|
+
- [x] ✅ Relocation: with `POSTGRES_AIOPS_HOME` set, `audit.db`, the undo store,
|
|
121
|
+
and `secrets.enc` all land under that directory.
|
|
122
|
+
- [ ] A tight poll loop trips the runaway budget guard rather than hammering the
|
|
123
|
+
server (verified in the mock suite; not re-run live).
|
|
124
|
+
|
|
125
|
+
### 7. Cleanup
|
|
126
|
+
- [x] ✅ The test indexes were dropped and the throwaway container removed; every
|
|
127
|
+
step above is present in the audit DB.
|
|
128
|
+
|
|
129
|
+
## Criteria to consider it live-verified
|
|
130
|
+
|
|
131
|
+
1. Every checklist box is ticked against at least one real PostgreSQL version,
|
|
132
|
+
and the version is recorded. **Current status: satisfied for PostgreSQL
|
|
133
|
+
16.14 for sections 1, 2 (except `pg_stat_statements` and replication), 3
|
|
134
|
+
(bloat/vacuum), 4 (index write + undo), 5 (vacuum), 6 and 7.**
|
|
135
|
+
2. Any field-shape mismatch found during a run is fixed and covered by a
|
|
136
|
+
regression test. **Current status: satisfied — the undo-replay bug found in
|
|
137
|
+
the live run was fixed and has a regression test.**
|
|
138
|
+
3. The run is written up with the date and package version, matching how the
|
|
139
|
+
product line records its other live-verified tools. **Current status:
|
|
140
|
+
satisfied.**
|
|
141
|
+
|
|
142
|
+
The remaining open boxes are the honest edge of the claim: `pg_stat_statements`
|
|
143
|
+
based analysis, replication reads, session termination, `REINDEX`, and
|
|
144
|
+
`ALTER SYSTEM` + undo have **not** been exercised against a live server.
|
|
145
|
+
|
|
146
|
+
## Notes for maintainers
|
|
147
|
+
|
|
148
|
+
- `postgres-aiops doctor` is the single fastest live entry point; start there.
|
|
149
|
+
- To close the `pg_stat_statements` gap, start the container with
|
|
150
|
+
`-c shared_preload_libraries=pg_stat_statements`, then
|
|
151
|
+
`CREATE EXTENSION pg_stat_statements;` and generate load before running
|
|
152
|
+
`analyze slow-query`.
|
|
153
|
+
- To close the blocking gap, hold a row lock in one `psql` session and block a
|
|
154
|
+
second on it — `analyze blocking` should name the first session's pid as the
|
|
155
|
+
root blocker.
|
|
156
|
+
- To close the replication gap, add a streaming standby (a second container with
|
|
157
|
+
`pg_basebackup`) and re-run `repl status` / `repl slots` / `repl wal`.
|
|
158
|
+
- The analyses also accept **injected records**, so exported rows from a cluster
|
|
159
|
+
you cannot write to still exercise section 3 without any write access.
|
|
@@ -20,14 +20,30 @@ from typing import Any, Optional
|
|
|
20
20
|
from mcp.server.fastmcp import FastMCP
|
|
21
21
|
|
|
22
22
|
from postgres_aiops.config import load_config
|
|
23
|
-
from postgres_aiops.connection import ConnectionManager, PgError
|
|
24
|
-
from postgres_aiops.governance import sanitize
|
|
23
|
+
from postgres_aiops.connection import ConnectionManager, PgConnectionLostError, PgError
|
|
24
|
+
from postgres_aiops.governance import mark_unknown, sanitize
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
_DOCTOR_HINT = "Run 'postgres-aiops doctor' to verify connectivity and credentials."
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# Long enough to carry the remediation sentence. These messages teach the
|
|
32
|
+
# caller what to do instead, and that clause comes last — a 300-char cap cut
|
|
33
|
+
# it off silently on every refusal long enough to need one.
|
|
34
|
+
_ERROR_MAX = 800
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Failures that leave the statement's fate genuinely undetermined. Raised
|
|
38
|
+
# only from the statement-executing path, so it means an ESTABLISHED link
|
|
39
|
+
# died mid-statement — not that the server was unreachable. The driver gives
|
|
40
|
+
# both the same class, so the connection layer discriminates by position and
|
|
41
|
+
# raises a dedicated class; this layer only has to recognise it.
|
|
42
|
+
# PostgreSQL rolls back on backend termination, so usually nothing landed —
|
|
43
|
+
# but a COMMIT whose acknowledgement was lost did land.
|
|
44
|
+
_UNDETERMINED_ERRORS = (PgConnectionLostError,)
|
|
45
|
+
|
|
46
|
+
|
|
31
47
|
def _safe_error(exc: Exception, tool: str) -> str:
|
|
32
48
|
"""Return an agent-safe error string; log full detail server-side only."""
|
|
33
49
|
logger.error("Tool %s failed", tool, exc_info=True)
|
|
@@ -41,7 +57,7 @@ def _safe_error(exc: Exception, tool: str) -> str:
|
|
|
41
57
|
PgError,
|
|
42
58
|
)
|
|
43
59
|
if isinstance(exc, _passthrough):
|
|
44
|
-
return sanitize(str(exc),
|
|
60
|
+
return sanitize(str(exc), _ERROR_MAX)
|
|
45
61
|
return f"{type(exc).__name__}: operation failed."
|
|
46
62
|
|
|
47
63
|
|
|
@@ -65,7 +81,13 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
65
81
|
return [{"error": msg, "hint": _DOCTOR_HINT}]
|
|
66
82
|
if shape == "str":
|
|
67
83
|
return f"Error: {msg} {_DOCTOR_HINT}"
|
|
68
|
-
|
|
84
|
+
payload = {"error": msg, "hint": _DOCTOR_HINT}
|
|
85
|
+
# Flatten the exception into a dict and its type is gone
|
|
86
|
+
# for good — so classify here, while it is still known,
|
|
87
|
+
# whether the operation may nonetheless have taken effect.
|
|
88
|
+
if isinstance(e, _UNDETERMINED_ERRORS):
|
|
89
|
+
return mark_unknown(payload)
|
|
90
|
+
return payload
|
|
69
91
|
|
|
70
92
|
return wrapper
|
|
71
93
|
|
|
@@ -75,7 +97,7 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
75
97
|
mcp = FastMCP(
|
|
76
98
|
"postgres-aiops",
|
|
77
99
|
instructions=(
|
|
78
|
-
"Governed PostgreSQL DBA operations
|
|
100
|
+
"Governed PostgreSQL DBA operations: a one-shot cluster "
|
|
79
101
|
"'overview'; server reads (version/settings/extensions/databases/roles); "
|
|
80
102
|
"activity (sessions, long-running queries, locks); query stats "
|
|
81
103
|
"(pg_stat_statements top-N, EXPLAIN); index and table health (unused / "
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""MCP server wrapping postgres-aiops operations (stdio transport).
|
|
2
|
+
|
|
3
|
+
Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
|
|
4
|
+
delegates to the ``postgres_aiops`` ops package and is wrapped with the
|
|
5
|
+
postgres-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
|
|
6
|
+
|
|
7
|
+
Standalone, self-governed PostgreSQL DBA operations (preview).
|
|
8
|
+
For PostgreSQL servers/clusters via psycopg 3.
|
|
9
|
+
|
|
10
|
+
Source: https://github.com/AIops-tools/Postgres-AIops
|
|
11
|
+
License: MIT
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
17
|
+
|
|
18
|
+
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
19
|
+
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
20
|
+
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
21
|
+
activity,
|
|
22
|
+
analysis,
|
|
23
|
+
indexes,
|
|
24
|
+
queries,
|
|
25
|
+
remediation,
|
|
26
|
+
replication,
|
|
27
|
+
server,
|
|
28
|
+
tables,
|
|
29
|
+
undo,
|
|
30
|
+
)
|
|
31
|
+
from postgres_aiops.governance import READ_ONLY_ENV, is_read_only
|
|
32
|
+
|
|
33
|
+
__all__ = ["mcp", "main", "_safe_error", "tool_errors", "apply_read_only"]
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def apply_read_only() -> list[str]:
|
|
39
|
+
"""Unregister every write tool when read-only mode is on.
|
|
40
|
+
|
|
41
|
+
The ``@governed_tool`` harness already refuses writes in this mode, so this
|
|
42
|
+
is a second layer — and the one that matters for smaller local models:
|
|
43
|
+
a tool absent from ``list_tools()`` cannot be hallucinated into a call,
|
|
44
|
+
whereas a tool that exists but refuses invites retry loops. It also gives a
|
|
45
|
+
compliance reviewer something checkable — the write tools are simply not
|
|
46
|
+
exposed.
|
|
47
|
+
|
|
48
|
+
``risk_level == "low"`` is the read/write discriminator; a smoke test
|
|
49
|
+
asserts it stays in agreement with each tool's ``[READ]``/``[WRITE]``
|
|
50
|
+
docstring tag so the two can never drift apart silently.
|
|
51
|
+
|
|
52
|
+
Returns the names that were removed (empty when not in read-only mode).
|
|
53
|
+
"""
|
|
54
|
+
if not is_read_only():
|
|
55
|
+
return []
|
|
56
|
+
registry = mcp._tool_manager._tools
|
|
57
|
+
dropped = [
|
|
58
|
+
name
|
|
59
|
+
for name, tool in registry.items()
|
|
60
|
+
if getattr(getattr(tool, "fn", None), "_risk_level", "low") != "low"
|
|
61
|
+
]
|
|
62
|
+
for name in dropped:
|
|
63
|
+
del registry[name]
|
|
64
|
+
if dropped:
|
|
65
|
+
logger.info(
|
|
66
|
+
"%s is set — read-only mode: %d write tool(s) not exposed",
|
|
67
|
+
READ_ONLY_ENV, len(dropped),
|
|
68
|
+
)
|
|
69
|
+
return dropped
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
"""Run the MCP server over stdio."""
|
|
74
|
+
logging.basicConfig(level=logging.INFO)
|
|
75
|
+
apply_read_only()
|
|
76
|
+
mcp.run(transport="stdio")
|
|
@@ -34,14 +34,22 @@ def slow_query_rca(
|
|
|
34
34
|
target: Target name from config; omit for the default.
|
|
35
35
|
"""
|
|
36
36
|
conn = None
|
|
37
|
+
source: dict = {}
|
|
37
38
|
if statements is None:
|
|
38
39
|
conn = _get_connection(target)
|
|
39
|
-
|
|
40
|
+
source = query_ops.top_queries(conn, order_by="total_time", limit=limit)
|
|
41
|
+
statements = source["statements"]
|
|
40
42
|
explain = None
|
|
41
43
|
if explain_sql:
|
|
42
44
|
conn = conn or _get_connection(target)
|
|
43
45
|
explain = query_ops.explain_query(conn, explain_sql, analyze=False)
|
|
44
|
-
|
|
46
|
+
result = ops.slow_query_rca(statements, explain=explain)
|
|
47
|
+
# The RCA reads a top-N; if that top-N was itself cut short the verdict is
|
|
48
|
+
# drawn from a partial view. Say so rather than let it read as complete.
|
|
49
|
+
if source:
|
|
50
|
+
result["sourceTruncated"] = source["truncated"]
|
|
51
|
+
result["sourceLimit"] = source["limit"]
|
|
52
|
+
return result
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
@mcp.tool()
|
|
@@ -62,9 +70,15 @@ def bloat_and_vacuum_analysis(
|
|
|
62
70
|
limit: How many tables to pull when not injected (default 50).
|
|
63
71
|
target: Target name from config; omit for the default.
|
|
64
72
|
"""
|
|
73
|
+
source: dict = {}
|
|
65
74
|
if tables is None:
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
source = table_ops.table_bloat(_get_connection(target), limit=limit)
|
|
76
|
+
tables = source["tables"]
|
|
77
|
+
result = ops.bloat_and_vacuum_analysis(tables)
|
|
78
|
+
if source:
|
|
79
|
+
result["sourceTruncated"] = source["truncated"]
|
|
80
|
+
result["sourceLimit"] = source["limit"]
|
|
81
|
+
return result
|
|
68
82
|
|
|
69
83
|
|
|
70
84
|
@mcp.tool()
|