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.
Files changed (48) hide show
  1. mcp_server/__init__.py +1 -0
  2. mcp_server/_shared.py +107 -0
  3. mcp_server/server.py +38 -0
  4. mcp_server/tools/__init__.py +1 -0
  5. mcp_server/tools/analysis.py +148 -0
  6. mcp_server/tools/overview.py +23 -0
  7. mcp_server/tools/rabbit_reads.py +95 -0
  8. mcp_server/tools/redis_reads.py +98 -0
  9. mcp_server/tools/undo.py +121 -0
  10. mcp_server/tools/writes.py +355 -0
  11. queue_aiops/__init__.py +14 -0
  12. queue_aiops/cli/__init__.py +9 -0
  13. queue_aiops/cli/_common.py +78 -0
  14. queue_aiops/cli/_root.py +62 -0
  15. queue_aiops/cli/analyze.py +85 -0
  16. queue_aiops/cli/doctor.py +21 -0
  17. queue_aiops/cli/init.py +176 -0
  18. queue_aiops/cli/overview.py +16 -0
  19. queue_aiops/cli/rabbit_cmds.py +234 -0
  20. queue_aiops/cli/redis_cmds.py +147 -0
  21. queue_aiops/cli/secret.py +105 -0
  22. queue_aiops/cli/undo.py +62 -0
  23. queue_aiops/config.py +175 -0
  24. queue_aiops/connection.py +277 -0
  25. queue_aiops/doctor.py +98 -0
  26. queue_aiops/governance/__init__.py +40 -0
  27. queue_aiops/governance/audit.py +377 -0
  28. queue_aiops/governance/budget.py +225 -0
  29. queue_aiops/governance/decorators.py +482 -0
  30. queue_aiops/governance/paths.py +23 -0
  31. queue_aiops/governance/patterns.py +378 -0
  32. queue_aiops/governance/policy.py +430 -0
  33. queue_aiops/governance/sanitize.py +45 -0
  34. queue_aiops/governance/undo.py +218 -0
  35. queue_aiops/ops/__init__.py +1 -0
  36. queue_aiops/ops/_util.py +55 -0
  37. queue_aiops/ops/analysis.py +591 -0
  38. queue_aiops/ops/overview.py +90 -0
  39. queue_aiops/ops/rabbit_reads.py +218 -0
  40. queue_aiops/ops/redis_reads.py +237 -0
  41. queue_aiops/ops/writes.py +284 -0
  42. queue_aiops/platform.py +217 -0
  43. queue_aiops/secretstore.py +304 -0
  44. queue_aiops-0.1.0.dist-info/METADATA +168 -0
  45. queue_aiops-0.1.0.dist-info/RECORD +48 -0
  46. queue_aiops-0.1.0.dist-info/WHEEL +4 -0
  47. queue_aiops-0.1.0.dist-info/entry_points.txt +3 -0
  48. 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ queue-aiops = queue_aiops.cli:app
3
+ queue-aiops-mcp = mcp_server.server:main
@@ -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.