postgres-aiops 0.5.0__tar.gz → 0.7.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.5.0 → postgres_aiops-0.7.0}/CHANGELOG.md +19 -0
- postgres_aiops-0.7.0/Dockerfile +9 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/PKG-INFO +30 -42
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/README.md +28 -40
- postgres_aiops-0.7.0/RELEASE_NOTES.md +33 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/SECURITY.md +6 -3
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/docs/VERIFICATION.md +10 -7
- postgres_aiops-0.7.0/glama.json +6 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/_shared.py +11 -7
- postgres_aiops-0.7.0/mcp_server/server.py +51 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/remediation.py +1 -1
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/undo.py +40 -12
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/_common.py +10 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/init.py +0 -38
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/query.py +7 -4
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/remediate.py +31 -18
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/undo.py +10 -4
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/config.py +14 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/__init__.py +2 -6
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/budget.py +19 -22
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/decorators.py +61 -101
- postgres_aiops-0.7.0/postgres_aiops/governance/policy.py +67 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/queries.py +43 -7
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/pyproject.toml +2 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/server.json +2 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/skills/postgres-aiops/SKILL.md +13 -7
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/skills/postgres-aiops/references/agent-guardrails.md +15 -7
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/skills/postgres-aiops/references/capabilities.md +1 -1
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/skills/postgres-aiops/references/cli-reference.md +8 -5
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/skills/postgres-aiops/references/setup-guide.md +3 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/conftest.py +6 -4
- postgres_aiops-0.7.0/tests/test_cli_remediate.py +220 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_cli_writes.py +36 -3
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_gov_decorators.py +11 -28
- postgres_aiops-0.7.0/tests/test_gov_policy.py +56 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_governance_persistence.py +23 -35
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_init.py +7 -18
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_reads.py +33 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_smoke.py +59 -3
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_undo_executor.py +75 -2
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/uv.lock +54 -51
- postgres_aiops-0.5.0/RELEASE_NOTES.md +0 -50
- postgres_aiops-0.5.0/mcp_server/server.py +0 -76
- postgres_aiops-0.5.0/postgres_aiops/governance/policy.py +0 -430
- postgres_aiops-0.5.0/postgres_aiops/governance/readonly.py +0 -49
- postgres_aiops-0.5.0/tests/test_cli_remediate.py +0 -134
- postgres_aiops-0.5.0/tests/test_gov_policy.py +0 -415
- postgres_aiops-0.5.0/tests/test_readonly.py +0 -145
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/.github/workflows/mcp-publish.yml +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/.github/workflows/publish.yml +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/.gitignore +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/LICENSE +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/__init__.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/__init__.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/activity.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/analysis.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/indexes.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/queries.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/replication.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/server.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/mcp_server/tools/tables.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/__init__.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/__init__.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/_root.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/activity.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/analyze.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/doctor.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/index.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/overview.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/replication.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/secret.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/server.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/cli/table.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/connection.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/doctor.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/audit.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/outcome.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/paths.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/patterns.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/sanitize.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/governance/undo.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/__init__.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/_util.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/activity.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/analysis.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/indexes.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/overview.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/remediation.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/replication.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/server.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/ops/tables.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/postgres_aiops/secretstore.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/smithery.yaml +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_analysis.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_cli_reads.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_cli_secret.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_config.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_connection.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_connection_lost.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_connection_more.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_doctor.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_gov_audit.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_gov_patterns.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_mcp_tools.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_ops_more.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_optional_fields.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_secretstore.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_secretstore_more.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_self_lockout.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_truncation.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_undo_replay.py +0 -0
- {postgres_aiops-0.5.0 → postgres_aiops-0.7.0}/tests/test_writes.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.0 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
### Changed (BREAKING)
|
|
6
|
+
- **Requires MCP SDK 2.0** (`mcp[cli]>=2.0,<3.0`). `mcp.server.fastmcp` no longer exists in 2.0; the server is now built with `MCPServer` and reports its package version in the stdio handshake.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **`undo apply` works from the CLI.** Every write tool is imported lazily inside its own CLI command, so a CLI-driven undo ran in a process where the inverse tool was never registered and failed with "inverse tool is not registered" — for every write tool. Only the MCP entry point, which imports the whole server, worked. Found while live-verifying against a real cluster.
|
|
10
|
+
- **An undetermined outcome is audited `unknown`, not `ok`.** The harness only classified a result as undetermined when the payload *also* carried an `error` key, so a write that looked successful but had not been confirmed was recorded as a success.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v0.6.0 — 2026-07-21
|
|
14
|
+
|
|
15
|
+
### Changed (BREAKING)
|
|
16
|
+
- **Removed the authorization layer** — read-only mode, the approver gate, and rules.yaml deny are gone. The skill no longer decides read vs write; that is the agent's judgement or the connecting account's permissions. `<PREFIX>_READ_ONLY` now has no effect (a startup warning is logged); `<PREFIX>_AUDIT_APPROVED_BY`/`_RATIONALE` are optional audit annotations.
|
|
17
|
+
- The retained guarantee is **unbypassable audit over MCP and CLI alike** — no unaudited entry point. Harness = audit + runaway safety guard + undo + sanitize; `risk_level` is a descriptive audit label, not a gate.
|
|
18
|
+
|
|
19
|
+
See RELEASE_NOTES.md for tool-specific changes.
|
|
20
|
+
|
|
21
|
+
|
|
3
22
|
## v0.5.0 — 2026-07-20
|
|
4
23
|
|
|
5
24
|
### Fixed
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# Minimal image for Glama introspection: starts the MCP server over stdio.
|
|
3
|
+
# The tools/list introspection handshake needs no live PostgreSQL credentials.
|
|
4
|
+
FROM python:3.12-slim
|
|
5
|
+
|
|
6
|
+
RUN pip install --no-cache-dir postgres-aiops
|
|
7
|
+
|
|
8
|
+
# MCP server speaks JSON-RPC over stdio.
|
|
9
|
+
ENTRYPOINT ["postgres-aiops-mcp"]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: postgres-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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 Zhou <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: cryptography>=42.0
|
|
10
|
-
Requires-Dist: mcp[cli]<
|
|
10
|
+
Requires-Dist: mcp[cli]<3.0,>=2.0
|
|
11
11
|
Requires-Dist: psycopg[binary]<4.0,>=3.1
|
|
12
12
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
13
13
|
Requires-Dist: rich<16.0,>=13.0
|
|
@@ -22,8 +22,8 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
|
|
23
23
|
Governed AI-ops for **PostgreSQL DBA operations** — connecting to a server with
|
|
24
24
|
**psycopg 3** and reading the system catalogs and `pg_stat_*` views — with a
|
|
25
|
-
**built-in governance harness**: unified audit log,
|
|
26
|
-
budget guard, undo-token recording, and
|
|
25
|
+
**built-in governance harness**: unified audit log, token/runaway
|
|
26
|
+
budget guard, undo-token recording, and descriptive risk-tier labels.
|
|
27
27
|
Beyond the mock test suite, the reads, a governed write, and its undo have been
|
|
28
28
|
exercised against a live PostgreSQL 16.14 instance — see [`docs/VERIFICATION.md`](docs/VERIFICATION.md).
|
|
29
29
|
|
|
@@ -70,40 +70,25 @@ The flagship analyses accept injected records for pure/offline analysis, or pull
|
|
|
70
70
|
live from a configured target. `top_queries`/`slow_query_rca` require the
|
|
71
71
|
`pg_stat_statements` extension; the read role should have `pg_monitor`.
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## What this tool does, and does not, decide
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
It delivers PostgreSQL DBA operations — reads and writes — accurately and
|
|
76
|
+
efficiently, and records every one of them. It does **not** decide whether a
|
|
77
|
+
write is allowed to happen. That is the agent's judgement, or the permission of
|
|
78
|
+
the account you connect it with: connect with a PostgreSQL role that has no
|
|
79
|
+
write privileges (a read-only role, or one without INSERT/UPDATE/DELETE/DDL),
|
|
80
|
+
and the writes fail at the server — the place that actually owns the
|
|
81
|
+
permission.
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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`.
|
|
83
|
+
So there is no read-only switch, no policy file, no approval gate to configure.
|
|
84
|
+
The one thing the tool guarantees is that nothing is silent: **every call, over
|
|
85
|
+
MCP and over the CLI alike, lands an audit row** in `~/.postgres-aiops/audit.db`,
|
|
86
|
+
and destructive writes still capture their before-state and record an inverse
|
|
87
|
+
where one exists.
|
|
103
88
|
|
|
104
|
-
>
|
|
105
|
-
>
|
|
106
|
-
>
|
|
89
|
+
> Each tool declares a `risk_level`, carried into the audit row as a descriptive
|
|
90
|
+
> tier (none/confirm/review) — so a reviewer can see at a glance that a row was
|
|
91
|
+
> a high-risk delete. It is a label, not a gate.
|
|
107
92
|
|
|
108
93
|
Running a smaller / local model? See
|
|
109
94
|
[agent-guardrails.md](skills/postgres-aiops/references/agent-guardrails.md) — it lists
|
|
@@ -132,15 +117,18 @@ postgres-aiops-mcp
|
|
|
132
117
|
|
|
133
118
|
Every MCP tool passes through the bundled `@governed_tool` harness:
|
|
134
119
|
|
|
135
|
-
- **Audit** — every call (params, result, status, duration, risk tier,
|
|
136
|
-
rationale) is logged to `~/.postgres-aiops/audit.db`
|
|
137
|
-
`POSTGRES_AIOPS_HOME`).
|
|
138
|
-
|
|
139
|
-
- **
|
|
140
|
-
|
|
141
|
-
|
|
120
|
+
- **Audit** — every call (params, result, status, duration, risk tier, and any
|
|
121
|
+
operator-supplied approver/rationale) is logged to `~/.postgres-aiops/audit.db`
|
|
122
|
+
(relocatable via `POSTGRES_AIOPS_HOME`). The CLI writes the same row the MCP
|
|
123
|
+
path does — there is no unaudited entry point.
|
|
124
|
+
- **Runaway guard** — a safety backstop, not an authorization gate: the same call
|
|
125
|
+
hammered in a tight loop trips a circuit breaker. Disable with
|
|
126
|
+
`POSTGRES_RUNAWAY_MAX=0`; optional hard ceilings via `POSTGRES_MAX_TOOL_CALLS` /
|
|
127
|
+
`POSTGRES_MAX_TOOL_SECONDS`.
|
|
142
128
|
- **Undo recording** — reversible writes record an inverse descriptor built from
|
|
143
129
|
the fetched before-state.
|
|
130
|
+
- **Risk tier** — a descriptive label on the audit row derived from `risk_level`;
|
|
131
|
+
it gates nothing.
|
|
144
132
|
|
|
145
133
|
## Scope
|
|
146
134
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Governed AI-ops for **PostgreSQL DBA operations** — connecting to a server with
|
|
8
8
|
**psycopg 3** and reading the system catalogs and `pg_stat_*` views — with a
|
|
9
|
-
**built-in governance harness**: unified audit log,
|
|
10
|
-
budget guard, undo-token recording, and
|
|
9
|
+
**built-in governance harness**: unified audit log, token/runaway
|
|
10
|
+
budget guard, undo-token recording, and descriptive risk-tier labels.
|
|
11
11
|
Beyond the mock test suite, the reads, a governed write, and its undo have been
|
|
12
12
|
exercised against a live PostgreSQL 16.14 instance — see [`docs/VERIFICATION.md`](docs/VERIFICATION.md).
|
|
13
13
|
|
|
@@ -54,40 +54,25 @@ The flagship analyses accept injected records for pure/offline analysis, or pull
|
|
|
54
54
|
live from a configured target. `top_queries`/`slow_query_rca` require the
|
|
55
55
|
`pg_stat_statements` extension; the read role should have `pg_monitor`.
|
|
56
56
|
|
|
57
|
-
##
|
|
57
|
+
## What this tool does, and does not, decide
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
It delivers PostgreSQL DBA operations — reads and writes — accurately and
|
|
60
|
+
efficiently, and records every one of them. It does **not** decide whether a
|
|
61
|
+
write is allowed to happen. That is the agent's judgement, or the permission of
|
|
62
|
+
the account you connect it with: connect with a PostgreSQL role that has no
|
|
63
|
+
write privileges (a read-only role, or one without INSERT/UPDATE/DELETE/DDL),
|
|
64
|
+
and the writes fail at the server — the place that actually owns the
|
|
65
|
+
permission.
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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`.
|
|
67
|
+
So there is no read-only switch, no policy file, no approval gate to configure.
|
|
68
|
+
The one thing the tool guarantees is that nothing is silent: **every call, over
|
|
69
|
+
MCP and over the CLI alike, lands an audit row** in `~/.postgres-aiops/audit.db`,
|
|
70
|
+
and destructive writes still capture their before-state and record an inverse
|
|
71
|
+
where one exists.
|
|
87
72
|
|
|
88
|
-
>
|
|
89
|
-
>
|
|
90
|
-
>
|
|
73
|
+
> Each tool declares a `risk_level`, carried into the audit row as a descriptive
|
|
74
|
+
> tier (none/confirm/review) — so a reviewer can see at a glance that a row was
|
|
75
|
+
> a high-risk delete. It is a label, not a gate.
|
|
91
76
|
|
|
92
77
|
Running a smaller / local model? See
|
|
93
78
|
[agent-guardrails.md](skills/postgres-aiops/references/agent-guardrails.md) — it lists
|
|
@@ -116,15 +101,18 @@ postgres-aiops-mcp
|
|
|
116
101
|
|
|
117
102
|
Every MCP tool passes through the bundled `@governed_tool` harness:
|
|
118
103
|
|
|
119
|
-
- **Audit** — every call (params, result, status, duration, risk tier,
|
|
120
|
-
rationale) is logged to `~/.postgres-aiops/audit.db`
|
|
121
|
-
`POSTGRES_AIOPS_HOME`).
|
|
122
|
-
|
|
123
|
-
- **
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
- **Audit** — every call (params, result, status, duration, risk tier, and any
|
|
105
|
+
operator-supplied approver/rationale) is logged to `~/.postgres-aiops/audit.db`
|
|
106
|
+
(relocatable via `POSTGRES_AIOPS_HOME`). The CLI writes the same row the MCP
|
|
107
|
+
path does — there is no unaudited entry point.
|
|
108
|
+
- **Runaway guard** — a safety backstop, not an authorization gate: the same call
|
|
109
|
+
hammered in a tight loop trips a circuit breaker. Disable with
|
|
110
|
+
`POSTGRES_RUNAWAY_MAX=0`; optional hard ceilings via `POSTGRES_MAX_TOOL_CALLS` /
|
|
111
|
+
`POSTGRES_MAX_TOOL_SECONDS`.
|
|
126
112
|
- **Undo recording** — reversible writes record an inverse descriptor built from
|
|
127
113
|
the fetched before-state.
|
|
114
|
+
- **Risk tier** — a descriptive label on the audit row derived from `risk_level`;
|
|
115
|
+
it gates nothing.
|
|
128
116
|
|
|
129
117
|
## Scope
|
|
130
118
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Release notes — postgres-aiops 0.6.0
|
|
2
|
+
|
|
3
|
+
Previous release: 0.5.0.
|
|
4
|
+
|
|
5
|
+
## BREAKING — the authorization layer is removed
|
|
6
|
+
|
|
7
|
+
This tool no longer decides whether a write is permitted. Read-only mode
|
|
8
|
+
(`<PREFIX>_READ_ONLY`), the graduated-approval / approver gate, and the
|
|
9
|
+
`rules.yaml` deny engine are **all gone**. Whether an operation runs is the
|
|
10
|
+
agent's judgement, or the permission of the account you connect it with — point
|
|
11
|
+
it at a read-only credential and the write fails at the server, the place that
|
|
12
|
+
actually owns the permission.
|
|
13
|
+
|
|
14
|
+
What the tool guarantees instead is that **nothing is silent**: every operation,
|
|
15
|
+
over MCP **and** the CLI alike, lands a row in the audit log — there is no
|
|
16
|
+
unaudited entry point. Destructive writes still capture their before-state and
|
|
17
|
+
record an undo token where a clean inverse exists.
|
|
18
|
+
|
|
19
|
+
- If you set `<PREFIX>_READ_ONLY=1`, it now has **no effect** and the MCP server
|
|
20
|
+
logs a warning at startup. Restrict writes via the connecting account instead.
|
|
21
|
+
- `<PREFIX>_AUDIT_APPROVED_BY` / `<PREFIX>_AUDIT_RATIONALE` still work, but are now
|
|
22
|
+
**optional audit annotations** — recorded on the row when set, never required.
|
|
23
|
+
- The declared `risk_level` is carried into the audit row as a descriptive tier
|
|
24
|
+
(a label, not a gate).
|
|
25
|
+
|
|
26
|
+
The governance harness is now: **audit (MCP+CLI, unbypassable) · runaway/budget
|
|
27
|
+
safety guard · undo recording · output sanitize**. `policy.py` is a small
|
|
28
|
+
risk-tier classifier; `governance/readonly.py` is deleted.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Also in this tool
|
|
32
|
+
|
|
33
|
+
- **`explain --analyze` is now genuinely read-only.** ANALYZE executes the statement, so a `SELECT ... INTO` could create a table; it now runs inside a rolled-back transaction, so no side effect can persist.
|
|
@@ -46,9 +46,12 @@ Every MCP tool runs through the bundled `@governed_tool` harness
|
|
|
46
46
|
- **Token/runaway budget** — hard ceilings (`POSTGRES_MAX_TOOL_CALLS` /
|
|
47
47
|
`POSTGRES_MAX_TOOL_SECONDS` — the env-var names the bundled harness reads) plus
|
|
48
48
|
an on-by-default guard that trips a tight poll/retry loop.
|
|
49
|
-
- **
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
- **Risk tiers** — a descriptive label (`none`/`confirm`/`review`) carried into
|
|
50
|
+
the audit row from each tool's declared `risk_level`; it gates nothing. The
|
|
51
|
+
harness does not decide whether a write is permitted — that is the agent's
|
|
52
|
+
judgement or the connecting account's permissions. `POSTGRES_AUDIT_APPROVED_BY`
|
|
53
|
+
/ `POSTGRES_AUDIT_RATIONALE` are optional annotations recorded on the audit
|
|
54
|
+
row, never required, never blocking.
|
|
52
55
|
- **Undo-token recording** — reversible writes fetch the **real before-state
|
|
53
56
|
first** and record a faithful inverse (`create_index`↔`drop_index`, where drop
|
|
54
57
|
captures `pg_get_indexdef`; `update_setting` restores the prior value).
|
|
@@ -28,9 +28,10 @@ suite predicts. Sections left open below are **not** claimed.
|
|
|
28
28
|
(with `drop_index` capturing `pg_get_indexdef` **before** dropping), and
|
|
29
29
|
`update_setting` capturing the prior value. Irreversible ops (terminate,
|
|
30
30
|
cancel, vacuum, analyze, reindex, reset stats) declare no undo.
|
|
31
|
-
- Governance persistence: audited rows land in the SQLite audit DB
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
- Governance persistence: audited rows land in the SQLite audit DB. The harness
|
|
32
|
+
authorizes nothing — there is no read-only, deny-rule, or approver gate to
|
|
33
|
+
test; an approver, when supplied, is recorded on the audit row as an optional
|
|
34
|
+
annotation.
|
|
34
35
|
|
|
35
36
|
## Prerequisites for a live run
|
|
36
37
|
|
|
@@ -113,10 +114,12 @@ are open — record them as gaps rather than silently passing.
|
|
|
113
114
|
long-running backend (**open gap** — not exercised live).
|
|
114
115
|
- [ ] `remediate reindex` on a real index (**open gap**).
|
|
115
116
|
|
|
116
|
-
### 6. Governance
|
|
117
|
-
- [x] ✅
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
### 6. Governance records, it does not gate
|
|
118
|
+
- [x] ✅ The harness authorizes nothing — there is no read-only, deny-rule, or
|
|
119
|
+
approver gate to test. A high-risk write ran with no approver set and
|
|
120
|
+
landed an `ok` audit row; when `POSTGRES_AUDIT_APPROVED_BY` and
|
|
121
|
+
`POSTGRES_AUDIT_RATIONALE` were set, they appeared in the audit row as
|
|
122
|
+
optional annotations.
|
|
120
123
|
- [x] ✅ Relocation: with `POSTGRES_AIOPS_HOME` set, `audit.db`, the undo store,
|
|
121
124
|
and `secrets.enc` all land under that directory.
|
|
122
125
|
- [ ] A tight poll loop trips the runaway budget guard rather than hammering the
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
"""Shared MCP server primitives: the
|
|
1
|
+
"""Shared MCP server primitives: the MCPServer instance, connection helper,
|
|
2
2
|
error sanitisation, and the ``@tool_errors`` decorator.
|
|
3
3
|
|
|
4
4
|
Tool modules under ``mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
5
|
their ``@mcp.tool()`` functions onto it. ``mcp_server/server.py`` then imports
|
|
6
6
|
those modules and runs the server.
|
|
7
7
|
|
|
8
|
-
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any reflected tool
|
|
9
|
+
signature. mcp>=2.0 accepts the union, but the legacy form is retained for
|
|
10
|
+
consistency and to stay safe under any pydantic reflection path (踩坑 #33 —
|
|
11
|
+
older mcp/pydantic eval'd the union to ``types.UnionType`` and crashed the
|
|
12
|
+
``issubclass`` check).
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
import functools
|
|
@@ -17,8 +19,9 @@ from collections.abc import Callable
|
|
|
17
19
|
from pathlib import Path
|
|
18
20
|
from typing import Any, Optional
|
|
19
21
|
|
|
20
|
-
from mcp.server
|
|
22
|
+
from mcp.server import MCPServer
|
|
21
23
|
|
|
24
|
+
from postgres_aiops import __version__
|
|
22
25
|
from postgres_aiops.config import load_config
|
|
23
26
|
from postgres_aiops.connection import ConnectionManager, PgConnectionLostError, PgError
|
|
24
27
|
from postgres_aiops.governance import mark_unknown, sanitize
|
|
@@ -65,7 +68,7 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
65
68
|
"""Wrap a tool body in the canonical try/except → ``_safe_error`` pattern.
|
|
66
69
|
|
|
67
70
|
Place this *between* ``@governed_tool`` and the function so the audit
|
|
68
|
-
decorator and
|
|
71
|
+
decorator and MCPServer still see the original signature.
|
|
69
72
|
"""
|
|
70
73
|
|
|
71
74
|
def decorator(func: Callable) -> Callable:
|
|
@@ -94,8 +97,9 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
94
97
|
return decorator
|
|
95
98
|
|
|
96
99
|
|
|
97
|
-
mcp =
|
|
100
|
+
mcp = MCPServer(
|
|
98
101
|
"postgres-aiops",
|
|
102
|
+
version=__version__,
|
|
99
103
|
instructions=(
|
|
100
104
|
"Governed PostgreSQL DBA operations: a one-shot cluster "
|
|
101
105
|
"'overview'; server reads (version/settings/extensions/databases/roles); "
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import os
|
|
16
|
+
|
|
17
|
+
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
18
|
+
|
|
19
|
+
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
20
|
+
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
21
|
+
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
22
|
+
activity,
|
|
23
|
+
analysis,
|
|
24
|
+
indexes,
|
|
25
|
+
queries,
|
|
26
|
+
remediation,
|
|
27
|
+
replication,
|
|
28
|
+
server,
|
|
29
|
+
tables,
|
|
30
|
+
undo,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = ["mcp", "main", "_safe_error", "tool_errors"]
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
"""Run the MCP server over stdio."""
|
|
41
|
+
logging.basicConfig(level=logging.INFO)
|
|
42
|
+
# Read-only mode was removed. Warn a deployment that still exports the old
|
|
43
|
+
# switch so it gets one audible signal instead of silently gaining writes.
|
|
44
|
+
if os.environ.get("POSTGRES_READ_ONLY"):
|
|
45
|
+
logger.warning(
|
|
46
|
+
"POSTGRES_READ_ONLY is set but no longer has any effect — "
|
|
47
|
+
"read-only mode was removed. Writes ARE enabled. Restrict them via "
|
|
48
|
+
"the connecting account's permissions instead (a read-only socket / "
|
|
49
|
+
"scope-limited token)."
|
|
50
|
+
)
|
|
51
|
+
mcp.run(transport="stdio")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""PostgreSQL maintenance MCP tools (guarded writes).
|
|
2
2
|
|
|
3
3
|
The state-changing tools. Every one is wrapped with the governance harness
|
|
4
|
-
(audit +
|
|
4
|
+
(audit + risk-tier tagging) and takes a ``dry_run`` preview. Reversible
|
|
5
5
|
writes pass an ``undo=`` callback that turns the fetched before-state into an
|
|
6
6
|
inverse descriptor the harness records; irreversible ones record none.
|
|
7
7
|
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
Every governed write records a replayable inverse descriptor (tool + params) to
|
|
4
4
|
``undo.db``. This module closes the loop: ``undo_apply`` looks up a recorded
|
|
5
5
|
descriptor and dispatches it to the named governed tool, so the inverse runs on
|
|
6
|
-
the SAME governance path as any other call (audited, budget-checked, and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
the inner tool it calls.
|
|
6
|
+
the SAME governance path as any other call (audited, budget-checked, and
|
|
7
|
+
tagged with its own risk tier). ``undo_apply`` is itself governed; audit and
|
|
8
|
+
risk-tier labeling for the inverse are handled by the inner tool it calls.
|
|
10
9
|
|
|
11
10
|
Note: recorded ``undo_params`` are the redacted safe-params captured at record
|
|
12
11
|
time, so an inverse that would need a secret value cannot be replayed here — by
|
|
@@ -22,8 +21,22 @@ from postgres_aiops.governance.undo import get_undo_store
|
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
def _resolve_tool(name: str) -> Any:
|
|
25
|
-
"""Return the governed callable registered under ``name`` (or None).
|
|
24
|
+
"""Return the governed callable registered under ``name`` (or None).
|
|
25
|
+
|
|
26
|
+
The CLI reaches this through ``undo apply``, whose process only imports
|
|
27
|
+
``mcp_server.tools.undo`` — every other tool module is imported lazily inside
|
|
28
|
+
its own CLI command and so is absent here. Without forcing a full load,
|
|
29
|
+
``undo_apply`` would fail with "inverse tool not registered" for every write
|
|
30
|
+
tool when driven from the CLI (the MCP entry point loads all modules, so it
|
|
31
|
+
never hit this). If the first lookup misses, import ``mcp_server.server`` —
|
|
32
|
+
which registers every ``@mcp.tool()`` as an import side effect — and retry.
|
|
33
|
+
Imported lazily to avoid the undo↔server cycle.
|
|
34
|
+
"""
|
|
26
35
|
tool = mcp._tool_manager._tools.get(name)
|
|
36
|
+
if tool is None:
|
|
37
|
+
import mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
38
|
+
|
|
39
|
+
tool = mcp._tool_manager._tools.get(name)
|
|
27
40
|
return getattr(tool, "fn", None) if tool else None
|
|
28
41
|
|
|
29
42
|
|
|
@@ -73,9 +86,9 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
|
|
|
73
86
|
def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None) -> dict:
|
|
74
87
|
"""[WRITE][risk=medium] Apply a recorded undo by dispatching its inverse tool.
|
|
75
88
|
|
|
76
|
-
The inverse runs through its own governed tool, so its
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
The inverse runs through its own governed tool, so its risk tier and audit
|
|
90
|
+
row are recorded there. Pass dry_run=True to preview the inverse call
|
|
91
|
+
without executing it. A token can only be applied once.
|
|
79
92
|
|
|
80
93
|
Args:
|
|
81
94
|
undo_id: The undoId from undo_list (or an ``_undo_id`` in a write result).
|
|
@@ -92,12 +105,27 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
|
|
|
92
105
|
)
|
|
93
106
|
|
|
94
107
|
inverse_tool = rec["undo_tool"]
|
|
108
|
+
# Unreadable parameters must stop the replay, not be replaced with {}.
|
|
109
|
+
# Defaulting dispatched the inverse tool with NO ARGUMENTS: for a tool whose
|
|
110
|
+
# parameters all have defaults that runs a real, unintended operation, and
|
|
111
|
+
# dry_run previewed it as a legitimate no-arg inverse. Refusing is the only
|
|
112
|
+
# honest answer — the recorded intent is gone, so there is nothing to apply.
|
|
113
|
+
raw = rec["undo_params"]
|
|
95
114
|
try:
|
|
96
|
-
params = json.loads(
|
|
97
|
-
except (ValueError, TypeError):
|
|
98
|
-
|
|
115
|
+
params = json.loads(raw) if raw else {}
|
|
116
|
+
except (ValueError, TypeError) as exc:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Undo '{undo_id}' has unreadable recorded parameters, so its inverse "
|
|
119
|
+
f"cannot be replayed. Inspect the row in undo.db and re-run the "
|
|
120
|
+
f"inverse tool '{inverse_tool}' by hand if the change still needs "
|
|
121
|
+
f"reversing."
|
|
122
|
+
) from exc
|
|
99
123
|
if not isinstance(params, dict):
|
|
100
|
-
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Undo '{undo_id}' recorded parameters of type "
|
|
126
|
+
f"'{type(params).__name__}' rather than an object, so its inverse "
|
|
127
|
+
f"cannot be replayed. Re-run the inverse tool '{inverse_tool}' by hand."
|
|
128
|
+
)
|
|
101
129
|
|
|
102
130
|
fn = _resolve_tool(inverse_tool)
|
|
103
131
|
if fn is None:
|
|
@@ -27,10 +27,18 @@ DryRunOption = Annotated[
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _cli_error_types() -> tuple[type[BaseException], ...]:
|
|
30
|
-
"""Exceptions translated to a one-line teaching error instead of a traceback.
|
|
30
|
+
"""Exceptions translated to a one-line teaching error instead of a traceback.
|
|
31
|
+
|
|
32
|
+
``PolicyDenied``/``BudgetExceeded`` are raised by ``@governed_tool`` OUTSIDE
|
|
33
|
+
the tool body, so ``tool_errors`` never sees them and they never arrive as
|
|
34
|
+
an ``{"error": ...}`` dict. Their message is the teaching text (which
|
|
35
|
+
approver to set, which budget was hit) — without them here a refusal
|
|
36
|
+
reaches the CLI as a traceback instead.
|
|
37
|
+
"""
|
|
31
38
|
from postgres_aiops.connection import PgError
|
|
39
|
+
from postgres_aiops.governance import BudgetExceeded, PolicyDenied
|
|
32
40
|
|
|
33
|
-
return (PgError, KeyError, OSError, ValueError)
|
|
41
|
+
return (PgError, PolicyDenied, BudgetExceeded, KeyError, OSError, ValueError)
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
def cli_errors(fn: Callable) -> Callable:
|
|
@@ -22,45 +22,8 @@ from postgres_aiops.config import (
|
|
|
22
22
|
DEFAULT_SSLMODE,
|
|
23
23
|
DEFAULT_USER,
|
|
24
24
|
)
|
|
25
|
-
from postgres_aiops.governance.paths import ops_path
|
|
26
25
|
from postgres_aiops.secretstore import SecretStore, resolve_master_password
|
|
27
26
|
|
|
28
|
-
# Starter policy: keeps the secure-by-default gate (high/critical writes need a
|
|
29
|
-
# named approver) explicit and editable, and shows the other rule kinds.
|
|
30
|
-
DEFAULT_RULES_YAML = """\
|
|
31
|
-
# postgres-aiops policy rules — hot-reloaded on change (no restart needed).
|
|
32
|
-
# Kinds: deny rules, maintenance_window, risk_tiers (graduated autonomy).
|
|
33
|
-
|
|
34
|
-
risk_tiers:
|
|
35
|
-
- name: high-risk-requires-approver
|
|
36
|
-
tier: dual
|
|
37
|
-
min_risk_level: high
|
|
38
|
-
reason: >-
|
|
39
|
-
High/critical writes need a named human approver — set
|
|
40
|
-
POSTGRES_AUDIT_APPROVED_BY (and POSTGRES_AUDIT_RATIONALE) before the call.
|
|
41
|
-
|
|
42
|
-
# deny:
|
|
43
|
-
# - name: no-prod-drops
|
|
44
|
-
# operations: ["drop_*"]
|
|
45
|
-
# environments: ["production"]
|
|
46
|
-
# reason: "Drops in production go through change management."
|
|
47
|
-
|
|
48
|
-
# maintenance_window:
|
|
49
|
-
# start: "22:00"
|
|
50
|
-
# end: "06:00"
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _write_default_rules() -> None:
|
|
55
|
-
"""Seed a starter rules.yaml (only when none exists) so the policy layer
|
|
56
|
-
is explicit from day one; never overwrites an operator-authored file."""
|
|
57
|
-
rules_path = ops_path("rules.yaml")
|
|
58
|
-
if rules_path.exists():
|
|
59
|
-
return
|
|
60
|
-
rules_path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
rules_path.write_text(DEFAULT_RULES_YAML, "utf-8")
|
|
62
|
-
console.print(f"[green]✓ Wrote default policy rules:[/] {rules_path}")
|
|
63
|
-
|
|
64
27
|
|
|
65
28
|
def _load_existing_targets() -> list[dict]:
|
|
66
29
|
if not CONFIG_FILE.exists():
|
|
@@ -138,7 +101,6 @@ def init_cmd() -> None:
|
|
|
138
101
|
if not typer.confirm("\nAdd another target?", default=False):
|
|
139
102
|
break
|
|
140
103
|
|
|
141
|
-
_write_default_rules()
|
|
142
104
|
console.print(f"\n[green]✓ Setup complete.[/] Config: {CONFIG_FILE}")
|
|
143
105
|
console.print(
|
|
144
106
|
"[dim]Tip: export POSTGRES_AIOPS_MASTER_PASSWORD=... in your shell profile "
|
|
@@ -13,7 +13,7 @@ from postgres_aiops.cli._common import (
|
|
|
13
13
|
cli_errors,
|
|
14
14
|
console,
|
|
15
15
|
double_confirm,
|
|
16
|
-
|
|
16
|
+
dry_run_preview,
|
|
17
17
|
get_connection,
|
|
18
18
|
print_result,
|
|
19
19
|
)
|
|
@@ -62,13 +62,16 @@ def query_explain(
|
|
|
62
62
|
def query_reset(target: TargetOption = None, dry_run: DryRunOption = False) -> None:
|
|
63
63
|
"""Reset pg_stat_statements accumulators (irreversible; dry-run + confirm).
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
so
|
|
65
|
+
Both the preview and the real execution are delegated to the
|
|
66
|
+
``@governed_tool``-wrapped MCP function, so each is audited on the same
|
|
67
|
+
governance path as MCP calls.
|
|
67
68
|
"""
|
|
68
69
|
from mcp_server.tools import queries as gov
|
|
69
70
|
|
|
70
71
|
if dry_run:
|
|
71
|
-
|
|
72
|
+
dry_run_preview(
|
|
73
|
+
gov.reset_query_stats(dry_run=True, target=target),
|
|
74
|
+
operation="reset_query_stats", api_call="SELECT pg_stat_statements_reset()")
|
|
72
75
|
return
|
|
73
76
|
double_confirm("reset pg_stat_statements on", "this target")
|
|
74
77
|
console.print_json(json.dumps(gov.reset_query_stats(target=target)))
|