queue-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.
- mcp_server/__init__.py +1 -0
- mcp_server/_shared.py +107 -0
- mcp_server/server.py +38 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/analysis.py +148 -0
- mcp_server/tools/overview.py +23 -0
- mcp_server/tools/rabbit_reads.py +95 -0
- mcp_server/tools/redis_reads.py +98 -0
- mcp_server/tools/undo.py +121 -0
- mcp_server/tools/writes.py +355 -0
- queue_aiops/__init__.py +14 -0
- queue_aiops/cli/__init__.py +9 -0
- queue_aiops/cli/_common.py +78 -0
- queue_aiops/cli/_root.py +62 -0
- queue_aiops/cli/analyze.py +85 -0
- queue_aiops/cli/doctor.py +21 -0
- queue_aiops/cli/init.py +176 -0
- queue_aiops/cli/overview.py +16 -0
- queue_aiops/cli/rabbit_cmds.py +234 -0
- queue_aiops/cli/redis_cmds.py +147 -0
- queue_aiops/cli/secret.py +105 -0
- queue_aiops/cli/undo.py +62 -0
- queue_aiops/config.py +175 -0
- queue_aiops/connection.py +277 -0
- queue_aiops/doctor.py +98 -0
- queue_aiops/governance/__init__.py +40 -0
- queue_aiops/governance/audit.py +377 -0
- queue_aiops/governance/budget.py +225 -0
- queue_aiops/governance/decorators.py +482 -0
- queue_aiops/governance/paths.py +23 -0
- queue_aiops/governance/patterns.py +378 -0
- queue_aiops/governance/policy.py +430 -0
- queue_aiops/governance/sanitize.py +45 -0
- queue_aiops/governance/undo.py +218 -0
- queue_aiops/ops/__init__.py +1 -0
- queue_aiops/ops/_util.py +55 -0
- queue_aiops/ops/analysis.py +591 -0
- queue_aiops/ops/overview.py +90 -0
- queue_aiops/ops/rabbit_reads.py +218 -0
- queue_aiops/ops/redis_reads.py +237 -0
- queue_aiops/ops/writes.py +284 -0
- queue_aiops/platform.py +217 -0
- queue_aiops/secretstore.py +304 -0
- queue_aiops-0.1.0.dist-info/METADATA +168 -0
- queue_aiops-0.1.0.dist-info/RECORD +48 -0
- queue_aiops-0.1.0.dist-info/WHEEL +4 -0
- queue_aiops-0.1.0.dist-info/entry_points.txt +3 -0
- queue_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Encrypted secret storage for queue-aiops.
|
|
2
|
+
|
|
3
|
+
Secrets (redis / rabbitmq passwords) are NEVER written to disk in plaintext. They
|
|
4
|
+
live in ``~/.queue-aiops/secrets.enc`` encrypted with Fernet (AES-128-CBC +
|
|
5
|
+
HMAC-SHA256). The Fernet key is derived from a master password via scrypt —
|
|
6
|
+
the password itself is never stored; only a random per-store salt and the
|
|
7
|
+
ciphertext are on disk.
|
|
8
|
+
|
|
9
|
+
The master password is resolved (in order) from:
|
|
10
|
+
|
|
11
|
+
1. the env var ``QUEUE_AIOPS_MASTER_PASSWORD`` (for non-interactive use:
|
|
12
|
+
the MCP server, CI, cron), or
|
|
13
|
+
2. an interactive ``getpass`` prompt (CLI on a TTY).
|
|
14
|
+
|
|
15
|
+
This module is deliberately self-contained (no skill-family imports) so it can
|
|
16
|
+
be vendored per tool exactly like the governance harness. To adapt it for
|
|
17
|
+
another tool, change the three constants below.
|
|
18
|
+
|
|
19
|
+
Design rules honoured here:
|
|
20
|
+
* Immutability — the decrypted secret map is never mutated in place; every
|
|
21
|
+
``set``/``delete`` derives a new dict and re-encrypts it.
|
|
22
|
+
* Fail fast with teaching errors at the trust boundary.
|
|
23
|
+
* Owner-only files (chmod 600 on the blob, 700 on the dir).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import getpass
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import stat
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from cryptography.exceptions import InvalidKey
|
|
39
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
40
|
+
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
41
|
+
|
|
42
|
+
from queue_aiops.governance.paths import ops_home
|
|
43
|
+
|
|
44
|
+
# ─── Tool-specific constants (change these three to vendor for another tool) ──
|
|
45
|
+
APP_NAME = "queue-aiops"
|
|
46
|
+
CONFIG_DIR = ops_home()
|
|
47
|
+
MASTER_PASSWORD_ENV = "QUEUE_AIOPS_MASTER_PASSWORD" # nosec B105 — env-var name
|
|
48
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
SECRETS_FILE = CONFIG_DIR / "secrets.enc"
|
|
51
|
+
LEGACY_ENV_FILE = CONFIG_DIR / ".env"
|
|
52
|
+
|
|
53
|
+
# scrypt work factors (RFC 7914). N must be a power of two; these give ~100ms
|
|
54
|
+
# on commodity hardware in 2026 — strong for an interactive unlock, cheap
|
|
55
|
+
# enough to run once per process.
|
|
56
|
+
_SCRYPT_N = 2**15
|
|
57
|
+
_SCRYPT_R = 8
|
|
58
|
+
_SCRYPT_P = 1
|
|
59
|
+
_KEY_LEN = 32
|
|
60
|
+
_SALT_LEN = 16
|
|
61
|
+
_FORMAT_VERSION = 1
|
|
62
|
+
|
|
63
|
+
_log = logging.getLogger(f"{APP_NAME}.secretstore")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SecretStoreError(Exception):
|
|
67
|
+
"""A secret could not be stored or retrieved; carries a teaching message."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MasterPasswordError(SecretStoreError):
|
|
71
|
+
"""The master password is missing or wrong."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _derive_key(password: str, salt: bytes) -> bytes:
|
|
75
|
+
"""Derive a urlsafe-base64 Fernet key from a password + salt via scrypt."""
|
|
76
|
+
kdf = Scrypt(salt=salt, length=_KEY_LEN, n=_SCRYPT_N, r=_SCRYPT_R, p=_SCRYPT_P)
|
|
77
|
+
raw = kdf.derive(password.encode("utf-8"))
|
|
78
|
+
return base64.urlsafe_b64encode(raw)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _chmod_600(path: Path) -> None:
|
|
82
|
+
try:
|
|
83
|
+
path.chmod(0o600)
|
|
84
|
+
except OSError: # best effort on exotic filesystems
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _ensure_dir() -> None:
|
|
89
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
try:
|
|
91
|
+
CONFIG_DIR.chmod(0o700)
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_master_password(*, confirm_if_new: bool = False) -> str:
|
|
97
|
+
"""Resolve the master password from env or an interactive prompt.
|
|
98
|
+
|
|
99
|
+
``confirm_if_new`` asks twice when no store exists yet (set-up flow).
|
|
100
|
+
Raises ``MasterPasswordError`` when no password is available and there is no
|
|
101
|
+
TTY to prompt on (e.g. an MCP server started without the env var).
|
|
102
|
+
"""
|
|
103
|
+
env_value = os.environ.get(MASTER_PASSWORD_ENV)
|
|
104
|
+
if env_value:
|
|
105
|
+
return env_value
|
|
106
|
+
|
|
107
|
+
if not sys.stdin.isatty():
|
|
108
|
+
raise MasterPasswordError(
|
|
109
|
+
f"Master password not set. Export {MASTER_PASSWORD_ENV} (the password "
|
|
110
|
+
f"that unlocks {SECRETS_FILE}) before running non-interactively, or "
|
|
111
|
+
f"run '{APP_NAME} init' on a terminal to set up secrets."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
store_exists = SECRETS_FILE.exists()
|
|
115
|
+
prompt = f"Master password for {APP_NAME}: "
|
|
116
|
+
password = getpass.getpass(prompt)
|
|
117
|
+
if not password:
|
|
118
|
+
raise MasterPasswordError("Empty master password is not allowed.")
|
|
119
|
+
if confirm_if_new and not store_exists:
|
|
120
|
+
again = getpass.getpass("Confirm master password: ")
|
|
121
|
+
if again != password:
|
|
122
|
+
raise MasterPasswordError("Passwords did not match. Aborted.")
|
|
123
|
+
return password
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class SecretStore:
|
|
128
|
+
"""An unlocked view over the encrypted secret file.
|
|
129
|
+
|
|
130
|
+
Construct via :meth:`unlock` (or :func:`open_store`). The decrypted map is
|
|
131
|
+
held in memory only; every mutation returns a *new* ``SecretStore`` after
|
|
132
|
+
re-encrypting to disk — instances are immutable.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
_password: str
|
|
136
|
+
_salt: bytes
|
|
137
|
+
_data: dict[str, str]
|
|
138
|
+
|
|
139
|
+
# ── factory ────────────────────────────────────────────────────────────
|
|
140
|
+
@classmethod
|
|
141
|
+
def unlock(cls, password: str | None = None) -> SecretStore:
|
|
142
|
+
"""Open (or initialise) the store, decrypting with ``password``."""
|
|
143
|
+
pw = password if password is not None else resolve_master_password()
|
|
144
|
+
if not SECRETS_FILE.exists():
|
|
145
|
+
return cls(_password=pw, _salt=os.urandom(_SALT_LEN), _data={})
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
raw = json.loads(SECRETS_FILE.read_text("utf-8"))
|
|
149
|
+
except (OSError, ValueError) as exc:
|
|
150
|
+
raise SecretStoreError(
|
|
151
|
+
f"Could not read secret store {SECRETS_FILE}: {exc}"
|
|
152
|
+
) from exc
|
|
153
|
+
|
|
154
|
+
if raw.get("version") != _FORMAT_VERSION:
|
|
155
|
+
raise SecretStoreError(
|
|
156
|
+
f"Unsupported secret store version {raw.get('version')!r} in "
|
|
157
|
+
f"{SECRETS_FILE}; expected {_FORMAT_VERSION}."
|
|
158
|
+
)
|
|
159
|
+
salt = base64.b64decode(raw["salt"])
|
|
160
|
+
token = raw["ciphertext"].encode("ascii")
|
|
161
|
+
try:
|
|
162
|
+
key = _derive_key(pw, salt)
|
|
163
|
+
plaintext = Fernet(key).decrypt(token)
|
|
164
|
+
except (InvalidToken, InvalidKey, ValueError) as exc:
|
|
165
|
+
raise MasterPasswordError(
|
|
166
|
+
"Wrong master password (could not decrypt the secret store). "
|
|
167
|
+
f"If you forgot it, delete {SECRETS_FILE} and re-run "
|
|
168
|
+
f"'{APP_NAME} init' to re-enter credentials."
|
|
169
|
+
) from exc
|
|
170
|
+
data = json.loads(plaintext.decode("utf-8"))
|
|
171
|
+
return cls(_password=pw, _salt=salt, _data=dict(data))
|
|
172
|
+
|
|
173
|
+
# ── read ──────────────────────────────────────────────────────────────
|
|
174
|
+
def get(self, name: str) -> str:
|
|
175
|
+
try:
|
|
176
|
+
return self._data[name]
|
|
177
|
+
except KeyError as exc:
|
|
178
|
+
available = ", ".join(sorted(self._data)) or "(none)"
|
|
179
|
+
raise SecretStoreError(
|
|
180
|
+
f"No secret stored for '{name}'. Stored: {available}. "
|
|
181
|
+
f"Add one with '{APP_NAME} secret set {name}'."
|
|
182
|
+
) from exc
|
|
183
|
+
|
|
184
|
+
def names(self) -> tuple[str, ...]:
|
|
185
|
+
return tuple(sorted(self._data))
|
|
186
|
+
|
|
187
|
+
def __contains__(self, name: object) -> bool:
|
|
188
|
+
return name in self._data
|
|
189
|
+
|
|
190
|
+
# ── write (returns a new immutable store) ──────────────────────────────
|
|
191
|
+
def set(self, name: str, value: str) -> SecretStore:
|
|
192
|
+
if not name:
|
|
193
|
+
raise SecretStoreError("Secret name must not be empty.")
|
|
194
|
+
if not value:
|
|
195
|
+
raise SecretStoreError("Secret value must not be empty.")
|
|
196
|
+
new_data = {**self._data, name: value}
|
|
197
|
+
new_store = SecretStore(_password=self._password, _salt=self._salt, _data=new_data)
|
|
198
|
+
new_store._persist()
|
|
199
|
+
return new_store
|
|
200
|
+
|
|
201
|
+
def delete(self, name: str) -> SecretStore:
|
|
202
|
+
if name not in self._data:
|
|
203
|
+
raise SecretStoreError(f"No secret named '{name}' to delete.")
|
|
204
|
+
new_data = {k: v for k, v in self._data.items() if k != name}
|
|
205
|
+
new_store = SecretStore(_password=self._password, _salt=self._salt, _data=new_data)
|
|
206
|
+
new_store._persist()
|
|
207
|
+
return new_store
|
|
208
|
+
|
|
209
|
+
def with_password(self, new_password: str) -> SecretStore:
|
|
210
|
+
"""Re-encrypt all secrets under a new master password (rotation)."""
|
|
211
|
+
if not new_password:
|
|
212
|
+
raise SecretStoreError("New master password must not be empty.")
|
|
213
|
+
new_store = SecretStore(
|
|
214
|
+
_password=new_password, _salt=os.urandom(_SALT_LEN), _data=dict(self._data)
|
|
215
|
+
)
|
|
216
|
+
new_store._persist()
|
|
217
|
+
return new_store
|
|
218
|
+
|
|
219
|
+
# ── persistence ────────────────────────────────────────────────────────
|
|
220
|
+
def _persist(self) -> None:
|
|
221
|
+
_ensure_dir()
|
|
222
|
+
key = _derive_key(self._password, self._salt)
|
|
223
|
+
token = Fernet(key).encrypt(json.dumps(self._data).encode("utf-8"))
|
|
224
|
+
blob = json.dumps(
|
|
225
|
+
{
|
|
226
|
+
"version": _FORMAT_VERSION,
|
|
227
|
+
"salt": base64.b64encode(self._salt).decode("ascii"),
|
|
228
|
+
"ciphertext": token.decode("ascii"),
|
|
229
|
+
},
|
|
230
|
+
indent=2,
|
|
231
|
+
)
|
|
232
|
+
tmp = SECRETS_FILE.with_suffix(".enc.tmp")
|
|
233
|
+
tmp.write_text(blob, "utf-8")
|
|
234
|
+
_chmod_600(tmp)
|
|
235
|
+
tmp.replace(SECRETS_FILE)
|
|
236
|
+
_chmod_600(SECRETS_FILE)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ─── module-level convenience API (the rest of the app uses these) ───────────
|
|
240
|
+
|
|
241
|
+
_cached: SecretStore | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def open_store(password: str | None = None, *, use_cache: bool = True) -> SecretStore:
|
|
245
|
+
"""Return an unlocked store, caching it for the process when possible."""
|
|
246
|
+
global _cached # noqa: PLW0603
|
|
247
|
+
if use_cache and _cached is not None and password is None:
|
|
248
|
+
return _cached
|
|
249
|
+
store = SecretStore.unlock(password)
|
|
250
|
+
if use_cache and password is None:
|
|
251
|
+
_cached = store
|
|
252
|
+
return store
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_secret(name: str) -> str:
|
|
256
|
+
"""Look up a secret by name, unlocking the store on first use."""
|
|
257
|
+
return open_store().get(name)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def has_store() -> bool:
|
|
261
|
+
return SECRETS_FILE.exists()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_permissions() -> str | None:
|
|
265
|
+
"""Return a warning string if ``secrets.enc`` is group/world-accessible."""
|
|
266
|
+
if not SECRETS_FILE.exists():
|
|
267
|
+
return None
|
|
268
|
+
mode = SECRETS_FILE.stat().st_mode
|
|
269
|
+
if mode & (stat.S_IRWXG | stat.S_IRWXO):
|
|
270
|
+
return (
|
|
271
|
+
f"{SECRETS_FILE} has permissions {oct(stat.S_IMODE(mode))} "
|
|
272
|
+
f"(should be 600). Run: chmod 600 {SECRETS_FILE}"
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def migrate_legacy_env(prefix: str, suffix: str, password: str | None = None) -> list[str]:
|
|
278
|
+
"""Import plaintext ``.env`` secrets into the encrypted store.
|
|
279
|
+
|
|
280
|
+
Legacy keys look like ``QUEUE_<TARGET>_SECRET``; ``prefix='QUEUE_'`` and
|
|
281
|
+
``suffix='_PASSWORD'`` map them back to a target name. The plaintext
|
|
282
|
+
``.env`` is renamed to ``.env.migrated`` (chmod 600) so nothing is silently
|
|
283
|
+
lost. Returns the list of imported target names.
|
|
284
|
+
"""
|
|
285
|
+
if not LEGACY_ENV_FILE.exists():
|
|
286
|
+
return []
|
|
287
|
+
imported: list[str] = []
|
|
288
|
+
store = SecretStore.unlock(password)
|
|
289
|
+
for line in LEGACY_ENV_FILE.read_text("utf-8").splitlines():
|
|
290
|
+
line = line.strip()
|
|
291
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
292
|
+
continue
|
|
293
|
+
key, _, value = line.partition("=")
|
|
294
|
+
key, value = key.strip(), value.strip().strip("'\"")
|
|
295
|
+
if not (key.startswith(prefix) and key.endswith(suffix) and value):
|
|
296
|
+
continue
|
|
297
|
+
target = key[len(prefix) : -len(suffix)].lower().replace("_", "-")
|
|
298
|
+
store = store.set(target, value)
|
|
299
|
+
imported.append(target)
|
|
300
|
+
if imported:
|
|
301
|
+
backup = LEGACY_ENV_FILE.with_name(".env.migrated")
|
|
302
|
+
LEGACY_ENV_FILE.replace(backup)
|
|
303
|
+
_chmod_600(backup)
|
|
304
|
+
return imported
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: queue-aiops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
|
+
Author-email: wei <zhouwei008@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: cryptography>=42.0
|
|
10
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
11
|
+
Requires-Dist: mcp[cli]<2.0,>=1.10
|
|
12
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
13
|
+
Requires-Dist: redis<7.0,>=5.0
|
|
14
|
+
Requires-Dist: rich<16.0,>=13.0
|
|
15
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Queue AIops (preview)
|
|
19
|
+
|
|
20
|
+
**Governed AI-ops for redis + rabbitmq.** queue-aiops is for the team running
|
|
21
|
+
their own cache and message broker — a redis that "suddenly eats memory", a
|
|
22
|
+
rabbitmq whose queues quietly grow until publishers block — without an
|
|
23
|
+
enterprise observability suite. It gives an AI agent (or a human at the CLI) a
|
|
24
|
+
governed toolset over both: transparent root-cause analyses for memory
|
|
25
|
+
pressure, latency, queue backlog, and connection churn, plus the handful of
|
|
26
|
+
writes an operator actually needs (config set, client kill, purge/delete
|
|
27
|
+
queue, policies) — every call audited, budgeted, risk-tiered, and undo-recorded
|
|
28
|
+
by the built-in governance harness.
|
|
29
|
+
|
|
30
|
+
> **Disclaimer**: Community-maintained open-source project, **not affiliated
|
|
31
|
+
> with, endorsed by, or sponsored by the Redis or RabbitMQ projects or their
|
|
32
|
+
> respective owners.** Redis and RabbitMQ are trademarks of their respective owners.
|
|
33
|
+
>
|
|
34
|
+
> **Preview / mock-only**: not yet validated against production brokers. Both
|
|
35
|
+
> redis and rabbitmq are free/self-hostable (one lab container or package install each), so a lab
|
|
36
|
+
> check is easy — `queue-aiops doctor` is the fastest live probe.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install queue-aiops
|
|
42
|
+
|
|
43
|
+
queue-aiops init # wizard: pick platform (redis/rabbitmq), host/port, encrypted secret
|
|
44
|
+
queue-aiops doctor # config + secret + connectivity check (PING / /api/overview)
|
|
45
|
+
queue-aiops overview # one-shot health summary for the default target
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then the interesting parts:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
queue-aiops analyze memory # redis memory-pressure RCA (maxmemory, eviction, frag, big keys)
|
|
52
|
+
queue-aiops analyze latency # redis latency RCA (slowlog digest, fork/AOF stalls)
|
|
53
|
+
queue-aiops analyze backlog # rabbitmq queue-backlog RCA (consumers, unacked, watermarks)
|
|
54
|
+
queue-aiops analyze churn # connection churn, both platforms
|
|
55
|
+
queue-aiops redis bigkeys # SCAN-budgeted big-key sample (never KEYS *)
|
|
56
|
+
queue-aiops rabbitmq queues # deepest backlog first
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Support scope
|
|
60
|
+
|
|
61
|
+
| Platform | Protocol | Coverage |
|
|
62
|
+
|----------|----------|----------|
|
|
63
|
+
| **redis** (5.x–7.x wire protocol via `redis` Python client) | RESP, password optional, TLS optional | INFO (server/memory/clients/stats/persistence/keyspace), SLOWLOG, CLIENT LIST/KILL, CONFIG GET/SET, MEMORY STATS/USAGE, SCAN-budgeted big-key sampling, DBSIZE, PING |
|
|
64
|
+
| **rabbitmq** (management plugin HTTP API) | HTTP(S), Basic auth | /api/overview, /api/queues (+ per-vhost, detail, purge, declare, delete), /api/connections, /api/channels, /api/consumers, /api/policies (get/set/delete), /api/nodes |
|
|
65
|
+
|
|
66
|
+
**26 MCP tools** — 19 reads (incl. 4 flagship RCAs) + 7 governed writes.
|
|
67
|
+
|
|
68
|
+
| Group | Tools | R/W |
|
|
69
|
+
|-------|-------|:---:|
|
|
70
|
+
| Overview | queue_overview | read |
|
|
71
|
+
| redis reads | redis_server_info, redis_memory_stats, redis_clients, redis_slowlog, redis_config_get, redis_keyspace, redis_big_keys | read |
|
|
72
|
+
| rabbitmq reads | rabbitmq_overview, list_queues, queue_detail, list_connections, list_channels, list_policies, node_health | read |
|
|
73
|
+
| Flagship RCAs | redis_memory_pressure_rca, redis_latency_rca, rabbitmq_queue_backlog_rca, connection_churn_analysis | read |
|
|
74
|
+
| Writes (medium) | redis_config_set, redis_kill_client, declare_queue, set_policy, delete_policy | write |
|
|
75
|
+
| Writes (**high**) | purge_queue, delete_queue | write |
|
|
76
|
+
|
|
77
|
+
The four RCAs are transparent heuristics that report their numbers — thresholds
|
|
78
|
+
are named constants, every finding carries its evidence, never a black-box
|
|
79
|
+
verdict. Big-key sampling walks at most 10,000 keys with SCAN and sizes at most
|
|
80
|
+
200 with MEMORY USAGE — **never `KEYS *`** — and reports its coverage.
|
|
81
|
+
|
|
82
|
+
## Governance
|
|
83
|
+
|
|
84
|
+
Every MCP tool runs through the bundled `@governed_tool` harness
|
|
85
|
+
(`queue_aiops.governance` — no external dependency):
|
|
86
|
+
|
|
87
|
+
- **Audit** — every call lands in `~/.queue-aiops/audit.db` (relocatable via
|
|
88
|
+
`QUEUE_AIOPS_HOME`), secret-redacted.
|
|
89
|
+
- **Budget** — call/time ceilings (`QUEUE_MAX_TOOL_CALLS`,
|
|
90
|
+
`QUEUE_MAX_TOOL_SECONDS`) + a runaway-loop breaker.
|
|
91
|
+
- **Risk tiers & approval** — **secure by default**: with no
|
|
92
|
+
`~/.queue-aiops/rules.yaml`, high-risk writes (`purge_queue`,
|
|
93
|
+
`delete_queue`) are denied unless `QUEUE_AUDIT_APPROVED_BY` names an approver
|
|
94
|
+
(set `QUEUE_AUDIT_RATIONALE` too). `queue-aiops init` seeds a starter
|
|
95
|
+
rules.yaml with that dual-control tier; an operator-authored rules file is
|
|
96
|
+
honoured as-is.
|
|
97
|
+
- **Undo** — reversible writes capture the real before-state first:
|
|
98
|
+
`redis_config_set` records the prior value from CONFIG GET;
|
|
99
|
+
`set_policy`/`delete_policy` record the prior policy; `delete_queue` records
|
|
100
|
+
the queue's definition and its undo re-declares it (the **messages are not
|
|
101
|
+
restored** — the descriptor says so). Irreversible writes (`purge_queue`,
|
|
102
|
+
`redis_kill_client`) record priorState only.
|
|
103
|
+
- **Dry-run + double-confirm** — every write takes `dry_run=True` (MCP) /
|
|
104
|
+
`--dry-run` (CLI); CLI writes double-confirm and execute through the same
|
|
105
|
+
governed twins, so they land in the audit log too.
|
|
106
|
+
- Credentials live **encrypted** in `~/.queue-aiops/secrets.enc` (Fernet +
|
|
107
|
+
scrypt master password; `QUEUE_AIOPS_MASTER_PASSWORD` for non-interactive
|
|
108
|
+
use). Redis passwords are optional — an auth-less lab instance is a
|
|
109
|
+
supported target.
|
|
110
|
+
|
|
111
|
+
## MCP configuration
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"queue-aiops": {
|
|
117
|
+
"command": "uvx",
|
|
118
|
+
"args": ["--from", "queue-aiops", "queue-aiops-mcp"],
|
|
119
|
+
"env": {
|
|
120
|
+
"QUEUE_AIOPS_MASTER_PASSWORD": "your-master-password"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **env-block caveat**: MCP clients launch the server with a *minimal*
|
|
128
|
+
> environment — your shell profile is not sourced. Anything the server needs
|
|
129
|
+
> (`QUEUE_AIOPS_MASTER_PASSWORD`, a relocated `QUEUE_AIOPS_HOME`,
|
|
130
|
+
> `QUEUE_AUDIT_APPROVED_BY` for high-risk writes) must be set in the `env`
|
|
131
|
+
> block above, not just in your terminal.
|
|
132
|
+
|
|
133
|
+
Or, with the package installed: `queue-aiops mcp`.
|
|
134
|
+
|
|
135
|
+
## CLI reference (short)
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
queue-aiops init | doctor | overview | mcp
|
|
139
|
+
queue-aiops secret set|list|migrate ...
|
|
140
|
+
queue-aiops redis info|memory|clients|slowlog|config-get|keyspace|bigkeys
|
|
141
|
+
queue-aiops redis config-set <param> <value> [--dry-run]
|
|
142
|
+
queue-aiops redis kill-client --id <id> | --addr <ip:port> [--dry-run]
|
|
143
|
+
queue-aiops rabbitmq overview|queues|queue|connections|channels|policies|nodes
|
|
144
|
+
queue-aiops rabbitmq purge|delete-queue|declare-queue <name> [--vhost /] [--dry-run]
|
|
145
|
+
queue-aiops rabbitmq set-policy|delete-policy <name> ... [--dry-run]
|
|
146
|
+
queue-aiops analyze memory|latency|backlog|churn
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Verification status
|
|
150
|
+
|
|
151
|
+
Preview / mock-only: the full test suite runs against mocked clients (no live
|
|
152
|
+
broker needed), and the REST paths / INFO fields are modelled from the public
|
|
153
|
+
docs of both platforms. Not yet validated against live production brokers —
|
|
154
|
+
both are trivially self-hostable, so `queue-aiops doctor` against a
|
|
155
|
+
local lab redis instance / rabbitmq broker (management plugin enabled) is
|
|
156
|
+
the quickest live check.
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
缺功能提 issue/PR 欢迎留言 — missing a read you need (streams/consumer groups,
|
|
161
|
+
quorum-queue specifics, shovel/federation status), another broker platform, or
|
|
162
|
+
a threshold that doesn't fit your fleet? Open an issue or PR at
|
|
163
|
+
[github.com/AIops-tools/Queue-AIops](https://github.com/AIops-tools/Queue-AIops)
|
|
164
|
+
— platform registry entries are additive and small.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
mcp_server/__init__.py,sha256=XVUc40BzvXjOnX5B3XGaTpcCoLnoetmU71CDvwpt2GA,42
|
|
2
|
+
mcp_server/_shared.py,sha256=PDBWM3452U51poJEpk50rhbCcyLnJvjALmePVmyGRJk,4149
|
|
3
|
+
mcp_server/server.py,sha256=HF7N6SOlpMWAoEMJsvMFKK9D1A24yZjYuPqcXnuB78s,1276
|
|
4
|
+
mcp_server/tools/__init__.py,sha256=AZOqmTsbMg1fh8IsC9fv0NSDdkNA_ohgiyueeO5kjAo,76
|
|
5
|
+
mcp_server/tools/analysis.py,sha256=V68ennFNyvvTCCmH08cjOEqS-Q0cnln0LqHo-aU0xEg,6298
|
|
6
|
+
mcp_server/tools/overview.py,sha256=ARvuKrZkaB_ftHLfw4_mZik3VMvL2tBpT6ur-k04x9s,808
|
|
7
|
+
mcp_server/tools/rabbit_reads.py,sha256=tYmtrTJCTQ8y2ElEBsGEs1-WmCFw6esBfhnUWorZ9Ag,3093
|
|
8
|
+
mcp_server/tools/redis_reads.py,sha256=QG0xV2LGUqnXx7hRIB41LmefHPD5DaN1A4vhylXKv5w,3116
|
|
9
|
+
mcp_server/tools/undo.py,sha256=bVThzb2-OkWy0axlmQH9YdxDlrFHFAKV8dG3ONoImMg,4361
|
|
10
|
+
mcp_server/tools/writes.py,sha256=LNxU82ZSx4s5_rnkx2yHLJsbAGAC7gFGqadW6VBfdDk,13291
|
|
11
|
+
queue_aiops/__init__.py,sha256=plKtVKs1akljMEgbKqTO1WcKtRqORDK9mmvw69CbrT0,569
|
|
12
|
+
queue_aiops/config.py,sha256=vLr1kvYFD3ZCcmyhwwbBU0znLBPhQDdrQJ8hS3u5G60,5977
|
|
13
|
+
queue_aiops/connection.py,sha256=H9wFAfIQZaKRGY3kuWUP8F-yb9HbMUOFpWhEY_8X-ks,11264
|
|
14
|
+
queue_aiops/doctor.py,sha256=4c-WVUGhAhlYAhMRWMdL_PyE7RIXv3hO8AowPNVymxY,3599
|
|
15
|
+
queue_aiops/platform.py,sha256=tM_0_hHao1bGV7oRuU0qEWagKDBZWvJhBF2jPVrrPeA,7685
|
|
16
|
+
queue_aiops/secretstore.py,sha256=Bh2doYSBCwXG4uT3NDdfzTCXouFjaycr9z10Qn1Bs9Y,11920
|
|
17
|
+
queue_aiops/cli/__init__.py,sha256=Qv9eFVZgh4sUGYyetNqkefMdSqkEmeS8cpFnL11kLxQ,200
|
|
18
|
+
queue_aiops/cli/_common.py,sha256=eHJRVxPBigt_QyRG_5toM8e12Dnpu0zAAxNTOiYb410,2840
|
|
19
|
+
queue_aiops/cli/_root.py,sha256=UcAfWr_6RWHaUrCEUURyQpEyyOiUSe5CnZdl5IDUzuU,1874
|
|
20
|
+
queue_aiops/cli/analyze.py,sha256=v86QhusS7g6VizY-r-Bfs-OTYXPlazmAWBC56QfoJ84,2819
|
|
21
|
+
queue_aiops/cli/doctor.py,sha256=aXv8sMEK4-RX7Vdd5ikQ-2q5VV0bXWepG82B9Fu6TMA,526
|
|
22
|
+
queue_aiops/cli/init.py,sha256=kNJ0gjAO8D_QhDYWmOBH2Nk6XnoJtBi_Lg3SHvByHgM,6794
|
|
23
|
+
queue_aiops/cli/overview.py,sha256=Kgimib1NKZzFBKAmgbeZvEY8QU0PrWqL6DagNwreC1Y,490
|
|
24
|
+
queue_aiops/cli/rabbit_cmds.py,sha256=w9WzrneA3vDjX15qlKkQoo89blini_QZE1UJYV4L3Oc,7828
|
|
25
|
+
queue_aiops/cli/redis_cmds.py,sha256=SY942IRolN2n82wVK4PcpeSH7ea2XSLx6xLYw_VmNt8,4610
|
|
26
|
+
queue_aiops/cli/secret.py,sha256=gRjJedKjAoASuUFyq3ap3vvsBoUfPSVW31s4PmbaNjY,3495
|
|
27
|
+
queue_aiops/cli/undo.py,sha256=GDXdu104Ip14HQ_3No-unUWPZ33WA3xzObLXxA-oJqk,1871
|
|
28
|
+
queue_aiops/governance/__init__.py,sha256=IKnWVdYXngwn2aRST7RK1eLsunlE9qbARMSownQwL-o,1499
|
|
29
|
+
queue_aiops/governance/audit.py,sha256=l_K8KOhRr4LtnvZgmaO4zkXjgrvm_UYCDe-hkJHYmFE,13540
|
|
30
|
+
queue_aiops/governance/budget.py,sha256=XZxoQltsjVrZ_Q5f3XwootFnzzThyiCPdcNMxBJGmF8,9142
|
|
31
|
+
queue_aiops/governance/decorators.py,sha256=gctPeffGdvUMFU14A_M-dHHLZHASRjHdxjIaaX14SsA,18456
|
|
32
|
+
queue_aiops/governance/paths.py,sha256=W68k2KZN8PBAxm819BNkDSIe9IomIL7D1q7ckN_KL3Y,709
|
|
33
|
+
queue_aiops/governance/patterns.py,sha256=vzdrn1--i5S8Q6DELf2xoJYLxKK5S_hpxSSpZ7par-M,15553
|
|
34
|
+
queue_aiops/governance/policy.py,sha256=mlzRK6s8LQYzNIQYrcqXe_L3L7qp0RL4WySKSbxJTc4,17215
|
|
35
|
+
queue_aiops/governance/sanitize.py,sha256=q8b_mbTEiojR3PmSS8JvHj5NnjOqz8SpXQ7XRBeG0KA,1728
|
|
36
|
+
queue_aiops/governance/undo.py,sha256=iOx6BCLimy3sQ5sMgpd4NKeTqogx6hBBB642QoIVKCU,7770
|
|
37
|
+
queue_aiops/ops/__init__.py,sha256=mr0nG_uSUHU8e6-t-vDfvXtoMPOyWmd844T2nKwibfw,76
|
|
38
|
+
queue_aiops/ops/_util.py,sha256=NJ28wq3s28O8LuE0gRikNep0J9VCQKnZjRSXxRK3T8E,1775
|
|
39
|
+
queue_aiops/ops/analysis.py,sha256=2CVulWsQl5hjvE6QhCCYPK12t-AtW2wiozKuxCuoX9M,26556
|
|
40
|
+
queue_aiops/ops/overview.py,sha256=xCEQyu_n7vOm8cDrL67XMmfFaAWWHYfwEAFmoGr19Bg,3122
|
|
41
|
+
queue_aiops/ops/rabbit_reads.py,sha256=O1B9MAHy38CzMbbd2GaDbRRIFdnY_kmyrPftuMW36cs,9006
|
|
42
|
+
queue_aiops/ops/redis_reads.py,sha256=m8bqfjXJk1Qe88f1wyCegg6xcTXO99cEz-SyTvGStzM,9386
|
|
43
|
+
queue_aiops/ops/writes.py,sha256=rvI1TcKwBxoTsdmReRYyJQZyiC7rfPGrYiVXpoeM8h4,10430
|
|
44
|
+
queue_aiops-0.1.0.dist-info/METADATA,sha256=FfzyhTjTok7A4vE2H7fAcE99NTuk4oyXiZsY-GqAIY8,8058
|
|
45
|
+
queue_aiops-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
46
|
+
queue_aiops-0.1.0.dist-info/entry_points.txt,sha256=wa1r7KIvAQeFrCYHJYspzoRZubLLEYFDWmI-fUS_zGA,93
|
|
47
|
+
queue_aiops-0.1.0.dist-info/licenses/LICENSE,sha256=-aN-YMNjW-2HFH71T7ohVfCJf9XeNX0MSVcPFpsgs_8,1083
|
|
48
|
+
queue_aiops-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wei <zhouwei008@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|