postgres-aiops 0.1.0__py3-none-any.whl

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 (57) hide show
  1. mcp_server/__init__.py +1 -0
  2. mcp_server/_shared.py +101 -0
  3. mcp_server/server.py +37 -0
  4. mcp_server/tools/__init__.py +1 -0
  5. mcp_server/tools/activity.py +52 -0
  6. mcp_server/tools/analysis.py +89 -0
  7. mcp_server/tools/indexes.py +65 -0
  8. mcp_server/tools/queries.py +60 -0
  9. mcp_server/tools/remediation.py +281 -0
  10. mcp_server/tools/replication.py +43 -0
  11. mcp_server/tools/server.py +86 -0
  12. mcp_server/tools/tables.py +46 -0
  13. postgres_aiops/__init__.py +9 -0
  14. postgres_aiops/cli/__init__.py +9 -0
  15. postgres_aiops/cli/_common.py +78 -0
  16. postgres_aiops/cli/_root.py +68 -0
  17. postgres_aiops/cli/activity.py +52 -0
  18. postgres_aiops/cli/analyze.py +53 -0
  19. postgres_aiops/cli/doctor.py +21 -0
  20. postgres_aiops/cli/index.py +55 -0
  21. postgres_aiops/cli/init.py +112 -0
  22. postgres_aiops/cli/overview.py +16 -0
  23. postgres_aiops/cli/query.py +70 -0
  24. postgres_aiops/cli/remediate.py +186 -0
  25. postgres_aiops/cli/replication.py +45 -0
  26. postgres_aiops/cli/secret.py +103 -0
  27. postgres_aiops/cli/server.py +69 -0
  28. postgres_aiops/cli/table.py +45 -0
  29. postgres_aiops/config.py +154 -0
  30. postgres_aiops/connection.py +178 -0
  31. postgres_aiops/doctor.py +84 -0
  32. postgres_aiops/governance/__init__.py +40 -0
  33. postgres_aiops/governance/audit.py +377 -0
  34. postgres_aiops/governance/budget.py +225 -0
  35. postgres_aiops/governance/decorators.py +474 -0
  36. postgres_aiops/governance/paths.py +23 -0
  37. postgres_aiops/governance/patterns.py +378 -0
  38. postgres_aiops/governance/policy.py +411 -0
  39. postgres_aiops/governance/sanitize.py +39 -0
  40. postgres_aiops/governance/undo.py +218 -0
  41. postgres_aiops/ops/__init__.py +1 -0
  42. postgres_aiops/ops/_util.py +102 -0
  43. postgres_aiops/ops/activity.py +193 -0
  44. postgres_aiops/ops/analysis.py +263 -0
  45. postgres_aiops/ops/indexes.py +211 -0
  46. postgres_aiops/ops/overview.py +51 -0
  47. postgres_aiops/ops/queries.py +123 -0
  48. postgres_aiops/ops/remediation.py +237 -0
  49. postgres_aiops/ops/replication.py +144 -0
  50. postgres_aiops/ops/server.py +151 -0
  51. postgres_aiops/ops/tables.py +146 -0
  52. postgres_aiops/secretstore.py +302 -0
  53. postgres_aiops-0.1.0.dist-info/METADATA +119 -0
  54. postgres_aiops-0.1.0.dist-info/RECORD +57 -0
  55. postgres_aiops-0.1.0.dist-info/WHEEL +4 -0
  56. postgres_aiops-0.1.0.dist-info/entry_points.txt +3 -0
  57. postgres_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,69 @@
1
+ """``postgres-aiops server`` — server-level reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from postgres_aiops.cli._common import TargetOption, cli_errors, console, get_connection
11
+
12
+ server_app = typer.Typer(
13
+ name="server",
14
+ help="Server reads: version, settings, extensions, databases, roles.",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ @server_app.command("version")
20
+ @cli_errors
21
+ def server_version(target: TargetOption = None) -> None:
22
+ """Server version, uptime and recovery state."""
23
+ from postgres_aiops.ops import server as ops
24
+
25
+ conn, _ = get_connection(target)
26
+ console.print_json(json.dumps(ops.server_version(conn)))
27
+
28
+
29
+ @server_app.command("settings")
30
+ @cli_errors
31
+ def server_settings(
32
+ pattern: Annotated[str | None, typer.Argument(help="Name substring filter")] = None,
33
+ target: TargetOption = None,
34
+ ) -> None:
35
+ """Configuration parameters (pg_settings), optionally filtered by name."""
36
+ from postgres_aiops.ops import server as ops
37
+
38
+ conn, _ = get_connection(target)
39
+ console.print_json(json.dumps(ops.show_settings(conn, pattern)))
40
+
41
+
42
+ @server_app.command("extensions")
43
+ @cli_errors
44
+ def server_extensions(target: TargetOption = None) -> None:
45
+ """Installed extensions."""
46
+ from postgres_aiops.ops import server as ops
47
+
48
+ conn, _ = get_connection(target)
49
+ console.print_json(json.dumps(ops.list_extensions(conn)))
50
+
51
+
52
+ @server_app.command("databases")
53
+ @cli_errors
54
+ def server_databases(target: TargetOption = None) -> None:
55
+ """Databases with owner, encoding and size."""
56
+ from postgres_aiops.ops import server as ops
57
+
58
+ conn, _ = get_connection(target)
59
+ console.print_json(json.dumps(ops.list_databases(conn)))
60
+
61
+
62
+ @server_app.command("roles")
63
+ @cli_errors
64
+ def server_roles(target: TargetOption = None) -> None:
65
+ """Roles and their attributes."""
66
+ from postgres_aiops.ops import server as ops
67
+
68
+ conn, _ = get_connection(target)
69
+ console.print_json(json.dumps(ops.list_roles(conn)))
@@ -0,0 +1,45 @@
1
+ """``postgres-aiops table`` — table-health reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import typer
8
+
9
+ from postgres_aiops.cli._common import TargetOption, cli_errors, console, get_connection
10
+
11
+ table_app = typer.Typer(
12
+ name="table",
13
+ help="Table health: sizes, bloat, autovacuum.",
14
+ no_args_is_help=True,
15
+ )
16
+
17
+
18
+ @table_app.command("sizes")
19
+ @cli_errors
20
+ def table_sizes(target: TargetOption = None) -> None:
21
+ """Largest tables by total relation size."""
22
+ from postgres_aiops.ops import tables as ops
23
+
24
+ conn, _ = get_connection(target)
25
+ console.print_json(json.dumps(ops.table_sizes(conn)))
26
+
27
+
28
+ @table_app.command("bloat")
29
+ @cli_errors
30
+ def table_bloat(target: TargetOption = None) -> None:
31
+ """Dead-tuple bloat proxy per table."""
32
+ from postgres_aiops.ops import tables as ops
33
+
34
+ conn, _ = get_connection(target)
35
+ console.print_json(json.dumps(ops.table_bloat(conn)))
36
+
37
+
38
+ @table_app.command("autovacuum")
39
+ @cli_errors
40
+ def table_autovacuum(target: TargetOption = None) -> None:
41
+ """Per-table dead tuples and last (auto)vacuum times."""
42
+ from postgres_aiops.ops import tables as ops
43
+
44
+ conn, _ = get_connection(target)
45
+ console.print_json(json.dumps(ops.autovacuum_status(conn)))
@@ -0,0 +1,154 @@
1
+ """Configuration management for Postgres AIops.
2
+
3
+ Loads connection targets from a YAML config file. The secret (the PostgreSQL
4
+ role **password**) is NEVER stored in the config file and never on disk in
5
+ plaintext: it lives in the encrypted store ``~/.postgres-aiops/secrets.enc``
6
+ (see :mod:`postgres_aiops.secretstore`). For backward compatibility a legacy
7
+ plaintext env var (``PG_<TARGET>_PASSWORD``) is still honoured as a fallback,
8
+ with a warning nudging migration to the encrypted store.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ import yaml
19
+
20
+ from postgres_aiops.secretstore import SecretStoreError, get_secret, has_store
21
+
22
+ CONFIG_DIR = Path.home() / ".postgres-aiops"
23
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
24
+ ENV_FILE = CONFIG_DIR / ".env"
25
+
26
+ DEFAULT_PORT = 5432
27
+ DEFAULT_DBNAME = "postgres"
28
+ DEFAULT_USER = "postgres"
29
+ DEFAULT_SSLMODE = "prefer"
30
+ APPLICATION_NAME = "postgres-aiops"
31
+
32
+ # Legacy env-var prefix/suffix; also used by the migration helper.
33
+ SECRET_ENV_PREFIX = "PG_" # nosec B105 — env-var name, not a secret
34
+ SECRET_ENV_SUFFIX = "_PASSWORD" # nosec B105 — env-var name, not a secret
35
+
36
+ _log = logging.getLogger("postgres-aiops.config")
37
+
38
+
39
+ def _secret_env_key(name: str) -> str:
40
+ """Legacy per-target password env var name, e.g. PG_PRIMARY_PASSWORD."""
41
+ return f"{SECRET_ENV_PREFIX}{name.upper().replace('-', '_')}{SECRET_ENV_SUFFIX}"
42
+
43
+
44
+ def _resolve_secret(name: str) -> str:
45
+ """Return a target's password: encrypted store first, then legacy env var."""
46
+ if has_store():
47
+ try:
48
+ return get_secret(name)
49
+ except SecretStoreError:
50
+ pass # fall through to legacy env var
51
+ legacy = os.environ.get(_secret_env_key(name))
52
+ if legacy:
53
+ _log.warning(
54
+ "Using plaintext env var %s. Migrate to the encrypted store with "
55
+ "'postgres-aiops secret migrate'.",
56
+ _secret_env_key(name),
57
+ )
58
+ return legacy
59
+ raise OSError(
60
+ f"No password for target '{name}'. Add one with "
61
+ f"'postgres-aiops secret set {name}' (stored encrypted), or run "
62
+ f"'postgres-aiops init'."
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class TargetConfig:
68
+ """A connection target for a PostgreSQL server/cluster.
69
+
70
+ The password is sourced from the encrypted secret store (see ``password``),
71
+ never the config file. ``host``/``port`` locate the server; ``dbname`` is the
72
+ database to connect to; ``sslmode`` follows libpq semantics
73
+ (disable/allow/prefer/require/verify-ca/verify-full).
74
+ """
75
+
76
+ name: str
77
+ host: str
78
+ port: int = DEFAULT_PORT
79
+ dbname: str = DEFAULT_DBNAME
80
+ user: str = DEFAULT_USER
81
+ sslmode: str = DEFAULT_SSLMODE
82
+
83
+ @property
84
+ def password(self) -> str:
85
+ return _resolve_secret(self.name)
86
+
87
+ @property
88
+ def conn_kwargs(self) -> dict:
89
+ """libpq connection keyword args for ``psycopg.connect`` (incl. password)."""
90
+ return {
91
+ "host": self.host,
92
+ "port": self.port,
93
+ "dbname": self.dbname,
94
+ "user": self.user,
95
+ "password": self.password,
96
+ "sslmode": self.sslmode,
97
+ "application_name": APPLICATION_NAME,
98
+ }
99
+
100
+ @property
101
+ def dsn_redacted(self) -> str:
102
+ """A human-readable DSN with the password redacted (for logs/doctor)."""
103
+ return (
104
+ f"postgresql://{self.user}:***@{self.host}:{self.port}/"
105
+ f"{self.dbname}?sslmode={self.sslmode}"
106
+ )
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class AppConfig:
111
+ """Top-level application config."""
112
+
113
+ targets: tuple[TargetConfig, ...] = ()
114
+
115
+ def get_target(self, name: str) -> TargetConfig:
116
+ for t in self.targets:
117
+ if t.name == name:
118
+ return t
119
+ available = ", ".join(t.name for t in self.targets) or "(none)"
120
+ raise KeyError(f"Target '{name}' not found. Available: {available}")
121
+
122
+ @property
123
+ def default_target(self) -> TargetConfig:
124
+ if not self.targets:
125
+ raise ValueError("No targets configured. Check config.yaml")
126
+ return self.targets[0]
127
+
128
+
129
+ def load_config(config_path: Path | None = None) -> AppConfig:
130
+ """Load config from YAML; the password comes from the encrypted store."""
131
+ path = config_path or CONFIG_FILE
132
+ if not path.exists():
133
+ raise FileNotFoundError(
134
+ f"Config file not found: {path}\n"
135
+ f"Run 'postgres-aiops init' to set up a target and store its password "
136
+ f"encrypted, or create {CONFIG_FILE} with a 'targets' list."
137
+ )
138
+
139
+ with open(path) as f:
140
+ raw = yaml.safe_load(f) or {}
141
+
142
+ targets = tuple(
143
+ TargetConfig(
144
+ name=t["name"],
145
+ host=t["host"],
146
+ port=t.get("port", DEFAULT_PORT),
147
+ dbname=t.get("dbname", DEFAULT_DBNAME),
148
+ user=t.get("user", DEFAULT_USER),
149
+ sslmode=t.get("sslmode", DEFAULT_SSLMODE),
150
+ )
151
+ for t in raw.get("targets", [])
152
+ )
153
+
154
+ return AppConfig(targets=targets)
@@ -0,0 +1,178 @@
1
+ """Connection management for PostgreSQL via psycopg 3.
2
+
3
+ A thin wrapper over a live libpq connection with per-target session reuse:
4
+
5
+ * Non-secret connection details (host / port / dbname / user / sslmode) come
6
+ from ``config.yaml``; the **password** is read from the encrypted secret
7
+ store (``~/.postgres-aiops/secrets.enc``) at connect time, never from disk in
8
+ plaintext.
9
+ * Reads run parameterised SQL against the system catalogs and ``pg_stat_*``
10
+ views; the connection is opened ``autocommit=True`` so maintenance commands
11
+ that cannot run inside a transaction block (``VACUUM``, ``CREATE INDEX
12
+ CONCURRENTLY``, ``REINDEX CONCURRENTLY``) work directly.
13
+ * Rows come back as dicts (``dict_row`` row factory), so the ops layer never
14
+ has to index columns positionally.
15
+
16
+ All ``psycopg`` errors are translated centrally into ``PgError`` with a teaching
17
+ message rather than leaking a raw traceback to an agent.
18
+
19
+ The underlying connection is injectable for tests: pass ``connection=`` to
20
+ ``PgConnection`` to substitute a fake that implements ``cursor()`` / ``close()``
21
+ — **no live database is required** to exercise the ops layer.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ import psycopg
29
+ from psycopg.rows import dict_row
30
+
31
+ from postgres_aiops.config import AppConfig, TargetConfig, load_config
32
+
33
+ _CONNECT_TIMEOUT = 10
34
+
35
+
36
+ class PgError(Exception):
37
+ """A PostgreSQL operation failed; carries a teaching message + sqlstate."""
38
+
39
+ def __init__(self, message: str, *, sqlstate: str | None = None) -> None:
40
+ self.sqlstate = sqlstate
41
+ super().__init__(message)
42
+
43
+
44
+ def _teaching_message(exc: psycopg.Error, target: TargetConfig) -> str:
45
+ """Map a psycopg error to an actionable, teaching message."""
46
+ sqlstate = getattr(exc, "sqlstate", None)
47
+ detail = str(exc).strip().splitlines()[0][:200] if str(exc) else ""
48
+ if isinstance(exc, psycopg.OperationalError):
49
+ return (
50
+ f"Could not connect to PostgreSQL at {target.host}:{target.port}/"
51
+ f"{target.dbname} as '{target.user}'. Check the host/port are reachable, "
52
+ f"the role/password are correct, and pg_hba.conf permits this client "
53
+ f"(sslmode={target.sslmode}). {detail}"
54
+ )
55
+ if sqlstate == "42P01": # undefined_table
56
+ return (
57
+ f"Relation not found ({sqlstate}). A required catalog/view is missing — "
58
+ f"pg_stat_statements must be installed (CREATE EXTENSION pg_stat_statements) "
59
+ f"for query stats. {detail}"
60
+ )
61
+ if sqlstate == "42501": # insufficient_privilege
62
+ return (
63
+ f"Insufficient privilege ({sqlstate}). This role lacks rights for the "
64
+ f"operation; a monitoring role needs pg_monitor, and maintenance "
65
+ f"commands need ownership. {detail}"
66
+ )
67
+ prefix = f" [{sqlstate}]" if sqlstate else ""
68
+ return f"PostgreSQL error{prefix} on {target.name}: {detail}"
69
+
70
+
71
+ class PgConnection:
72
+ """A single authenticated session against one PostgreSQL target."""
73
+
74
+ def __init__(self, target: TargetConfig, connection: Any | None = None) -> None:
75
+ self._target = target
76
+ self._conn = connection if connection is not None else self._open(target)
77
+
78
+ @staticmethod
79
+ def _open(target: TargetConfig) -> Any:
80
+ try:
81
+ return psycopg.connect(
82
+ **target.conn_kwargs,
83
+ row_factory=dict_row,
84
+ autocommit=True,
85
+ connect_timeout=_CONNECT_TIMEOUT,
86
+ )
87
+ except psycopg.Error as exc:
88
+ raise PgError(
89
+ _teaching_message(exc, target), sqlstate=getattr(exc, "sqlstate", None)
90
+ ) from exc
91
+
92
+ @property
93
+ def target(self) -> TargetConfig:
94
+ return self._target
95
+
96
+ def query(self, sql: str, params: Any | None = None) -> list[dict]:
97
+ """Run a read query and return rows as a list of dicts."""
98
+ try:
99
+ with self._conn.cursor() as cur:
100
+ cur.execute(sql, params)
101
+ return [dict(r) for r in cur.fetchall()]
102
+ except psycopg.Error as exc:
103
+ raise PgError(
104
+ _teaching_message(exc, self._target),
105
+ sqlstate=getattr(exc, "sqlstate", None),
106
+ ) from exc
107
+
108
+ def query_one(self, sql: str, params: Any | None = None) -> dict | None:
109
+ """Run a read query expected to return at most one row."""
110
+ rows = self.query(sql, params)
111
+ return rows[0] if rows else None
112
+
113
+ def scalar(self, sql: str, params: Any | None = None) -> Any:
114
+ """Run a read query and return the first column of the first row (or None)."""
115
+ row = self.query_one(sql, params)
116
+ if not row:
117
+ return None
118
+ return next(iter(row.values()), None)
119
+
120
+ def execute(self, sql: str, params: Any | None = None) -> str:
121
+ """Run a write/DDL/maintenance statement; return the libpq status message."""
122
+ try:
123
+ with self._conn.cursor() as cur:
124
+ cur.execute(sql, params)
125
+ return getattr(cur, "statusmessage", "") or "OK"
126
+ except psycopg.Error as exc:
127
+ raise PgError(
128
+ _teaching_message(exc, self._target),
129
+ sqlstate=getattr(exc, "sqlstate", None),
130
+ ) from exc
131
+
132
+ def close(self) -> None:
133
+ try:
134
+ self._conn.close()
135
+ except psycopg.Error:
136
+ pass
137
+
138
+
139
+ class ConnectionManager:
140
+ """Manages connections to multiple PostgreSQL targets with session reuse."""
141
+
142
+ def __init__(self, config: AppConfig) -> None:
143
+ self._config = config
144
+ self._connections: dict[str, PgConnection] = {}
145
+
146
+ @classmethod
147
+ def from_config(cls, config: AppConfig | None = None) -> ConnectionManager:
148
+ cfg = config or load_config()
149
+ return cls(cfg)
150
+
151
+ def connect(self, target_name: str | None = None) -> PgConnection:
152
+ """Connect to a target by name, or the default target."""
153
+ target = (
154
+ self._config.get_target(target_name)
155
+ if target_name
156
+ else self._config.default_target
157
+ )
158
+ cached = self._connections.get(target.name)
159
+ if cached is not None:
160
+ return cached
161
+ conn = PgConnection(target)
162
+ self._connections[target.name] = conn
163
+ return conn
164
+
165
+ def disconnect(self, target_name: str) -> None:
166
+ conn = self._connections.pop(target_name, None)
167
+ if conn is not None:
168
+ conn.close()
169
+
170
+ def disconnect_all(self) -> None:
171
+ for name in list(self._connections):
172
+ self.disconnect(name)
173
+
174
+ def list_targets(self) -> list[str]:
175
+ return [t.name for t in self._config.targets]
176
+
177
+ def list_connected(self) -> list[str]:
178
+ return list(self._connections.keys())
@@ -0,0 +1,84 @@
1
+ """Environment and connectivity diagnostics for Postgres AIops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+
7
+ from postgres_aiops.config import CONFIG_FILE, ENV_FILE, load_config
8
+ from postgres_aiops.secretstore import SECRETS_FILE, check_permissions, has_store
9
+
10
+ _console = Console()
11
+
12
+
13
+ def run_doctor(skip_auth: bool = False) -> int:
14
+ """Check config, secrets, and (optionally) connectivity.
15
+
16
+ Returns a process exit code: 0 healthy, 1 problems found. Connectivity
17
+ failures are reported as status, never raised as tracebacks (a doctor must
18
+ survive the thing it diagnoses being unhealthy).
19
+ """
20
+ problems = 0
21
+
22
+ if not CONFIG_FILE.exists():
23
+ _console.print(f"[red]✗ Config file missing: {CONFIG_FILE}[/]")
24
+ _console.print("[yellow] Run 'postgres-aiops init' to set up your first target.[/]")
25
+ return 1
26
+ _console.print(f"[green]✓ Config file present: {CONFIG_FILE}[/]")
27
+
28
+ try:
29
+ config = load_config()
30
+ except Exception as exc: # noqa: BLE001 — report, do not crash
31
+ _console.print(f"[red]✗ Config load failed: {exc}[/]")
32
+ return 1
33
+
34
+ if not config.targets:
35
+ _console.print("[red]✗ No targets configured[/]")
36
+ return 1
37
+ _console.print(f"[green]✓ {len(config.targets)} target(s) configured[/]")
38
+
39
+ if has_store():
40
+ _console.print(f"[green]✓ Encrypted secret store present: {SECRETS_FILE}[/]")
41
+ perm_warning = check_permissions()
42
+ if perm_warning:
43
+ _console.print(f"[yellow]! {perm_warning}[/]")
44
+ elif ENV_FILE.exists():
45
+ _console.print(
46
+ f"[yellow]! Using legacy plaintext .env ({ENV_FILE}). Migrate with "
47
+ f"'postgres-aiops secret migrate'.[/]"
48
+ )
49
+ else:
50
+ _console.print(
51
+ "[yellow]! No secret store yet. Run 'postgres-aiops init' to set up "
52
+ "credentials (stored encrypted).[/]"
53
+ )
54
+ problems += 1
55
+
56
+ for target in config.targets:
57
+ try:
58
+ _ = target.password
59
+ _console.print(f"[green]✓ password present for '{target.name}'[/]")
60
+ except OSError as exc:
61
+ _console.print(f"[red]✗ {exc}[/]")
62
+ problems += 1
63
+
64
+ if skip_auth:
65
+ _console.print("[dim]Skipping connectivity check (--skip-auth).[/]")
66
+ return 1 if problems else 0
67
+
68
+ from postgres_aiops.connection import ConnectionManager
69
+
70
+ mgr = ConnectionManager(config)
71
+ for target in config.targets:
72
+ try:
73
+ conn = mgr.connect(target.name)
74
+ row = conn.query_one("SELECT version() AS version") or {}
75
+ version = str(row.get("version", "?"))[:80]
76
+ _console.print(
77
+ f"[green]✓ Connected to '{target.name}' ({target.host}:{target.port}) "
78
+ f"— {version}[/]"
79
+ )
80
+ except Exception as exc: # noqa: BLE001 — connectivity is a status, not a crash
81
+ _console.print(f"[red]✗ Connect to '{target.name}' failed: {exc}[/]")
82
+ problems += 1
83
+
84
+ return 1 if problems else 0
@@ -0,0 +1,40 @@
1
+ """Governance harness for postgres-aiops — audit, policy, budget, undo, sanitize.
2
+
3
+ A self-contained, vendored governance layer. postgres-aiops has NO dependency on
4
+ any external skill family — this package is its own copy of the harness:
5
+
6
+ - ``@governed_tool`` — mandatory decorator on every MCP tool: policy pre-check,
7
+ token/runaway budget guard, graduated-autonomy risk-tier gate, audit logging,
8
+ and undo-token recording.
9
+ - unified SQLite audit log under ``~/.postgres-aiops/`` (override with
10
+ ``POSTGRES_AIOPS_HOME``).
11
+ - ``sanitize`` — prompt-injection defense for API-returned text.
12
+
13
+ State lives under ``ops_home()`` (default ``~/.postgres-aiops``).
14
+ """
15
+
16
+ from postgres_aiops.governance.audit import AuditEngine, get_engine
17
+ from postgres_aiops.governance.budget import BudgetExceeded, BudgetTracker, get_budget
18
+ from postgres_aiops.governance.decorators import PolicyDenied, governed_tool
19
+ from postgres_aiops.governance.patterns import Pattern, PatternMatch, get_pattern_engine
20
+ from postgres_aiops.governance.policy import TierDecision, get_policy_engine
21
+ from postgres_aiops.governance.sanitize import sanitize
22
+ from postgres_aiops.governance.undo import UndoStore, get_undo_store
23
+
24
+ __all__ = [
25
+ "governed_tool",
26
+ "sanitize",
27
+ "PolicyDenied",
28
+ "get_engine",
29
+ "AuditEngine",
30
+ "get_policy_engine",
31
+ "TierDecision",
32
+ "get_budget",
33
+ "BudgetTracker",
34
+ "BudgetExceeded",
35
+ "get_undo_store",
36
+ "UndoStore",
37
+ "Pattern",
38
+ "PatternMatch",
39
+ "get_pattern_engine",
40
+ ]