postgres-aiops 0.2.1__tar.gz → 0.4.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.
Files changed (106) hide show
  1. postgres_aiops-0.4.0/.github/workflows/mcp-publish.yml +55 -0
  2. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/.gitignore +1 -0
  3. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/CHANGELOG.md +5 -0
  4. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/PKG-INFO +54 -8
  5. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/README.md +53 -7
  6. postgres_aiops-0.4.0/RELEASE_NOTES.md +48 -0
  7. postgres_aiops-0.4.0/docs/VERIFICATION.md +159 -0
  8. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/_shared.py +1 -1
  9. postgres_aiops-0.4.0/mcp_server/server.py +76 -0
  10. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/analysis.py +18 -4
  11. postgres_aiops-0.4.0/mcp_server/tools/undo.py +126 -0
  12. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/_common.py +27 -0
  13. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/_root.py +2 -0
  14. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/analyze.py +22 -8
  15. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/index.py +13 -3
  16. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/query.py +2 -1
  17. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/table.py +22 -8
  18. postgres_aiops-0.4.0/postgres_aiops/cli/undo.py +62 -0
  19. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/__init__.py +5 -1
  20. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/decorators.py +12 -0
  21. postgres_aiops-0.4.0/postgres_aiops/governance/readonly.py +49 -0
  22. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/sanitize.py +26 -0
  23. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/_util.py +23 -2
  24. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/activity.py +28 -28
  25. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/analysis.py +6 -0
  26. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/indexes.py +22 -4
  27. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/queries.py +20 -5
  28. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/remediation.py +8 -8
  29. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/replication.py +15 -15
  30. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/server.py +8 -8
  31. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/tables.py +69 -21
  32. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/pyproject.toml +1 -1
  33. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/server.json +3 -3
  34. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/skills/postgres-aiops/SKILL.md +45 -19
  35. postgres_aiops-0.4.0/skills/postgres-aiops/references/agent-guardrails.md +111 -0
  36. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/skills/postgres-aiops/references/capabilities.md +6 -4
  37. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/skills/postgres-aiops/references/cli-reference.md +28 -8
  38. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/skills/postgres-aiops/references/setup-guide.md +1 -1
  39. postgres_aiops-0.4.0/tests/test_cli_reads.py +257 -0
  40. postgres_aiops-0.4.0/tests/test_cli_remediate.py +84 -0
  41. postgres_aiops-0.4.0/tests/test_cli_secret.py +98 -0
  42. postgres_aiops-0.4.0/tests/test_config.py +115 -0
  43. postgres_aiops-0.4.0/tests/test_connection_more.py +171 -0
  44. postgres_aiops-0.4.0/tests/test_gov_audit.py +292 -0
  45. postgres_aiops-0.4.0/tests/test_gov_decorators.py +452 -0
  46. postgres_aiops-0.4.0/tests/test_gov_patterns.py +417 -0
  47. postgres_aiops-0.4.0/tests/test_gov_policy.py +415 -0
  48. postgres_aiops-0.4.0/tests/test_mcp_tools.py +224 -0
  49. postgres_aiops-0.4.0/tests/test_ops_more.py +135 -0
  50. postgres_aiops-0.4.0/tests/test_optional_fields.py +197 -0
  51. postgres_aiops-0.4.0/tests/test_readonly.py +145 -0
  52. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_reads.py +3 -1
  53. postgres_aiops-0.4.0/tests/test_secretstore_more.py +140 -0
  54. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_smoke.py +6 -0
  55. postgres_aiops-0.4.0/tests/test_truncation.py +162 -0
  56. postgres_aiops-0.4.0/tests/test_undo_executor.py +138 -0
  57. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/uv.lock +1 -1
  58. postgres_aiops-0.2.1/.coverage +0 -0
  59. postgres_aiops-0.2.1/.github/workflows/mcp-publish.yml +0 -23
  60. postgres_aiops-0.2.1/RELEASE_NOTES.md +0 -52
  61. postgres_aiops-0.2.1/mcp_server/server.py +0 -37
  62. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/.github/workflows/publish.yml +0 -0
  63. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/LICENSE +0 -0
  64. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/SECURITY.md +0 -0
  65. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/__init__.py +0 -0
  66. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/__init__.py +0 -0
  67. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/activity.py +0 -0
  68. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/indexes.py +0 -0
  69. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/queries.py +0 -0
  70. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/remediation.py +0 -0
  71. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/replication.py +0 -0
  72. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/server.py +0 -0
  73. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/mcp_server/tools/tables.py +0 -0
  74. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/__init__.py +0 -0
  75. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/__init__.py +0 -0
  76. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/activity.py +0 -0
  77. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/doctor.py +0 -0
  78. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/init.py +0 -0
  79. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/overview.py +0 -0
  80. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/remediate.py +0 -0
  81. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/replication.py +0 -0
  82. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/secret.py +0 -0
  83. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/cli/server.py +0 -0
  84. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/config.py +0 -0
  85. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/connection.py +0 -0
  86. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/doctor.py +0 -0
  87. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/audit.py +0 -0
  88. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/budget.py +0 -0
  89. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/paths.py +0 -0
  90. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/patterns.py +0 -0
  91. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/policy.py +0 -0
  92. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/governance/undo.py +0 -0
  93. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/__init__.py +0 -0
  94. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/ops/overview.py +0 -0
  95. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/postgres_aiops/secretstore.py +0 -0
  96. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/smithery.yaml +0 -0
  97. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/conftest.py +0 -0
  98. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_analysis.py +0 -0
  99. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_cli_writes.py +0 -0
  100. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_connection.py +0 -0
  101. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_doctor.py +0 -0
  102. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_governance_persistence.py +0 -0
  103. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_init.py +0 -0
  104. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_secretstore.py +0 -0
  105. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_undo_replay.py +0 -0
  106. {postgres_aiops-0.2.1 → postgres_aiops-0.4.0}/tests/test_writes.py +0 -0
@@ -0,0 +1,55 @@
1
+ name: mcp-publish
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ release:
6
+ types: [published]
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ publish-mcp:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Wait for PyPI
18
+ # The release event also triggers the PyPI publish workflow; the MCP
19
+ # registry validates that the package version exists on PyPI, so poll
20
+ # until it has propagated (every 15s, up to 10 minutes).
21
+ run: |
22
+ python3 - <<'EOF'
23
+ import json
24
+ import sys
25
+ import time
26
+ import urllib.error
27
+ import urllib.request
28
+
29
+ with open("server.json", encoding="utf-8") as f:
30
+ package = json.load(f)["packages"][0]
31
+ name, version = package["identifier"], package["version"]
32
+ url = f"https://pypi.org/pypi/{name}/{version}/json"
33
+ deadline = time.monotonic() + 600
34
+ while True:
35
+ try:
36
+ with urllib.request.urlopen(url, timeout=10):
37
+ print(f"{name}=={version} is available on PyPI.")
38
+ sys.exit(0)
39
+ except (urllib.error.URLError, OSError) as exc:
40
+ print(f"{name}=={version} not on PyPI yet ({exc}); "
41
+ "retrying in 15s...")
42
+ if time.monotonic() >= deadline:
43
+ sys.exit(f"Timed out after 10 minutes waiting for "
44
+ f"{name}=={version} to appear on PyPI. "
45
+ "Check the PyPI publish workflow, then re-run "
46
+ "this workflow via workflow_dispatch.")
47
+ time.sleep(15)
48
+ EOF
49
+ - name: Install mcp-publisher
50
+ run: |
51
+ curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
52
+ - name: Login to MCP Registry (GitHub OIDC)
53
+ run: ./mcp-publisher login github-oidc
54
+ - name: Publish server.json
55
+ run: ./mcp-publisher publish
@@ -5,3 +5,4 @@ __pycache__/
5
5
  .pytest_cache/
6
6
  .ruff_cache/
7
7
  *.egg-info/
8
+ .coverage
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0 — 2026-07-17
4
+
5
+ ### Added
6
+ - **Undo executor**: `undo list` / `undo apply <id>` (CLI + MCP) — apply a recorded replayable inverse; the dispatched inverse is re-gated by its own risk tier; single-use, dry-run, double-confirm, both wrapper + inverse audited.
7
+
3
8
  ## v0.2.1 — 2026-07-16
4
9
 
5
10
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: postgres-aiops
3
- Version: 0.2.1
3
+ Version: 0.4.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
5
  Author-email: wei <zhouwei008@gmail.com>
6
6
  License-Expression: MIT
@@ -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 (preview)
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
- **Preview — mock-validated only, not run against a live cluster.**
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`): **33 tools** (24 read, 9 write), every one wrapped with the bundled `@governed_tool` harness.
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 (33 MCP tools)
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
- **Preview — mock-validated only, not run against a live cluster.** The catalog
118
- queries are modelled from the documented `pg_catalog` / `pg_stat_*` shapes and
119
- need live verification. `postgres-aiops doctor` is the fastest live check.
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 (preview)
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
- **Preview — mock-validated only, not run against a live cluster.**
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`): **33 tools** (24 read, 9 write), every one wrapped with the bundled `@governed_tool` harness.
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 (33 MCP tools)
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
- **Preview — mock-validated only, not run against a live cluster.** The catalog
102
- queries are modelled from the documented `pg_catalog` / `pg_stat_*` shapes and
103
- need live verification. `postgres-aiops doctor` is the fastest live check.
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,48 @@
1
+ # Release notes — postgres-aiops 0.4.0
2
+
3
+ Previous release: 0.3.0.
4
+
5
+ ## Headline: read-only mode
6
+
7
+ ```bash
8
+ export POSTGRES_READ_ONLY=1
9
+ ```
10
+
11
+ With this set the **10 write tools are never registered** — an MCP
12
+ client lists **25 tools instead of 35**. The writes are not hidden
13
+ behind a flag and not merely refused on call: they are absent from the session,
14
+ so a model cannot invoke one and cannot be argued into one. For a reviewer this
15
+ is checkable rather than promised — connect, list the tools, and the writes are
16
+ not there.
17
+
18
+ Enforcement is two layers deep: the `@governed_tool` harness refuses every
19
+ non-read operation (covering the CLI and in-process callers too), and the MCP
20
+ server removes write tools from `list_tools()`. Changing entry point does not
21
+ get around it.
22
+
23
+ ## BREAKING — return shapes changed
24
+
25
+ This release changes payloads that callers may be parsing. Both changes exist
26
+ to stop a result from misrepresenting itself:
27
+
28
+ 1. **Absent fields are now `null`, not `""`.** A missing value and an empty value
29
+ were previously indistinguishable, which invited consumers to invent the
30
+ difference. Keys are still always present — only the value may be null.
31
+ 2. **Anything with a `limit` now returns an envelope** —
32
+ `{"<items>": [...], "returned": N, "limit": L, "truncated": bool}`. Truncation is
33
+ *measured* (one extra row is fetched), never inferred from the page happening to
34
+ be full. Where a genuine pre-cap total is knowable it is reported as `total`;
35
+ where it isn't, `total` is deliberately omitted rather than echoing `returned`.
36
+
37
+ ## Also in this release
38
+
39
+ - **`docs/VERIFICATION.md`** — what the mock suite actually guarantees, a live
40
+ verification checklist, and the criteria for claiming this tool verified.
41
+ - **`skills/postgres-aiops/references/agent-guardrails.md`** — for driving this tool with a
42
+ smaller / local model: which guardrails are now enforced for you, and a
43
+ ready-made system prompt for the rest.
44
+ - Expanded operator playbooks in the skill documentation.
45
+ - The advertised tool count now matches what an MCP client actually lists
46
+ (it includes `undo_list` / `undo_apply`), and a release gate keeps it honest.
47
+ - The `(preview)` label has been dropped. It never meant unreleased; verification
48
+ status now lives in `docs/VERIFICATION.md` where it can be specific.
@@ -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.
@@ -75,7 +75,7 @@ def tool_errors(shape: str = "dict") -> Callable:
75
75
  mcp = FastMCP(
76
76
  "postgres-aiops",
77
77
  instructions=(
78
- "Governed PostgreSQL DBA operations (preview): a one-shot cluster "
78
+ "Governed PostgreSQL DBA operations: a one-shot cluster "
79
79
  "'overview'; server reads (version/settings/extensions/databases/roles); "
80
80
  "activity (sessions, long-running queries, locks); query stats "
81
81
  "(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
- statements = query_ops.top_queries(conn, order_by="total_time", limit=limit)["statements"]
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
- return ops.slow_query_rca(statements, explain=explain)
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
- tables = table_ops.table_bloat(_get_connection(target), limit=limit)["tables"]
67
- return ops.bloat_and_vacuum_analysis(tables)
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()