queue-aiops 0.1.1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/PKG-INFO +61 -12
  2. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/README.md +60 -11
  3. queue_aiops-0.2.1/RELEASE_NOTES.md +26 -0
  4. queue_aiops-0.2.1/docs/VERIFICATION.md +34 -0
  5. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/_shared.py +1 -1
  6. queue_aiops-0.2.1/mcp_server/server.py +76 -0
  7. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/rabbit_reads.py +16 -0
  8. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/redis_reads.py +12 -0
  9. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/undo.py +13 -2
  10. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/pyproject.toml +1 -1
  11. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/rabbit_cmds.py +28 -4
  12. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/redis_cmds.py +14 -2
  13. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/__init__.py +5 -1
  14. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/decorators.py +12 -0
  15. queue_aiops-0.2.1/queue_aiops/governance/readonly.py +49 -0
  16. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/sanitize.py +25 -0
  17. queue_aiops-0.2.1/queue_aiops/ops/_util.py +94 -0
  18. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/analysis.py +13 -10
  19. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/rabbit_reads.py +54 -33
  20. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/redis_reads.py +63 -35
  21. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/writes.py +8 -8
  22. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/server.json +3 -3
  23. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/SKILL.md +106 -37
  24. queue_aiops-0.2.1/skills/queue-aiops/references/agent-guardrails.md +109 -0
  25. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/capabilities.md +8 -1
  26. queue_aiops-0.2.1/tests/test_cli_rabbit_writes.py +164 -0
  27. queue_aiops-0.2.1/tests/test_cli_reads.py +249 -0
  28. queue_aiops-0.2.1/tests/test_cli_secret.py +111 -0
  29. queue_aiops-0.2.1/tests/test_connection.py +241 -0
  30. queue_aiops-0.2.1/tests/test_gov_audit.py +292 -0
  31. queue_aiops-0.2.1/tests/test_gov_decorators.py +452 -0
  32. queue_aiops-0.2.1/tests/test_gov_patterns.py +417 -0
  33. queue_aiops-0.2.1/tests/test_gov_policy.py +415 -0
  34. queue_aiops-0.2.1/tests/test_mcp_reads.py +211 -0
  35. queue_aiops-0.2.1/tests/test_optional_fields.py +184 -0
  36. queue_aiops-0.2.1/tests/test_readonly.py +142 -0
  37. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_reads.py +33 -2
  38. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/uv.lock +1 -1
  39. queue_aiops-0.1.1/RELEASE_NOTES.md +0 -33
  40. queue_aiops-0.1.1/mcp_server/server.py +0 -38
  41. queue_aiops-0.1.1/queue_aiops/ops/_util.py +0 -55
  42. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.github/workflows/mcp-publish.yml +0 -0
  43. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.github/workflows/publish.yml +0 -0
  44. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.gitignore +0 -0
  45. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/CHANGELOG.md +0 -0
  46. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/LICENSE +0 -0
  47. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/SECURITY.md +0 -0
  48. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/__init__.py +0 -0
  49. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/__init__.py +0 -0
  50. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/analysis.py +0 -0
  51. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/overview.py +0 -0
  52. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/writes.py +0 -0
  53. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/__init__.py +0 -0
  54. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/__init__.py +0 -0
  55. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/_common.py +0 -0
  56. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/_root.py +0 -0
  57. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/analyze.py +0 -0
  58. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/doctor.py +0 -0
  59. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/init.py +0 -0
  60. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/overview.py +0 -0
  61. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/secret.py +0 -0
  62. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/undo.py +0 -0
  63. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/config.py +0 -0
  64. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/connection.py +0 -0
  65. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/doctor.py +0 -0
  66. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/audit.py +0 -0
  67. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/budget.py +0 -0
  68. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/paths.py +0 -0
  69. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/patterns.py +0 -0
  70. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/policy.py +0 -0
  71. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/undo.py +0 -0
  72. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/__init__.py +0 -0
  73. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/overview.py +0 -0
  74. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/platform.py +0 -0
  75. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/secretstore.py +0 -0
  76. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/cli-reference.md +0 -0
  77. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/setup-guide.md +0 -0
  78. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/smithery.yaml +0 -0
  79. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/conftest.py +0 -0
  80. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/fakes.py +0 -0
  81. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_analysis.py +0 -0
  82. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_cli_writes.py +0 -0
  83. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_doctor.py +0 -0
  84. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_governance_persistence.py +0 -0
  85. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_init.py +0 -0
  86. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_platform.py +0 -0
  87. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_secretstore.py +0 -0
  88. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_smoke.py +0 -0
  89. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_undo_executor.py +0 -0
  90. {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_writes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queue-aiops
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
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
5
  Author-email: wei <zhouwei008@gmail.com>
6
6
  License-Expression: MIT
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
 
18
18
  <!-- mcp-name: io.github.AIops-tools/queue-aiops -->
19
19
 
20
- # Queue AIops (preview)
20
+ # Queue AIops
21
21
 
22
22
  **Governed AI-ops for redis + rabbitmq.** queue-aiops is for the team running
23
23
  their own cache and message broker — a redis that "suddenly eats memory", a
@@ -33,9 +33,10 @@ by the built-in governance harness.
33
33
  > with, endorsed by, or sponsored by the Redis or RabbitMQ projects or their
34
34
  > respective owners.** Redis and RabbitMQ are trademarks of their respective owners.
35
35
  >
36
- > **Preview / mock-only**: not yet validated against production brokers. Both
37
- > redis and rabbitmq are free/self-hostable (one lab container or package install each), so a lab
38
- > check is easy — `queue-aiops doctor` is the fastest live probe.
36
+ > **Verification**: behaviour is covered by a mock-based test suite; not yet validated
37
+ > against live brokers. Both redis and rabbitmq are free/self-hostable (one lab container
38
+ > or package install each), so a lab check is easy — `queue-aiops doctor` is the fastest
39
+ > live probe, and [docs/VERIFICATION.md](docs/VERIFICATION.md) is the checklist.
39
40
 
40
41
  ## Quick start
41
42
 
@@ -58,6 +59,46 @@ queue-aiops redis bigkeys # SCAN-budgeted big-key sample (never KEYS *)
58
59
  queue-aiops rabbitmq queues # deepest backlog first
59
60
  ```
60
61
 
62
+ ## Security: read-only mode
63
+
64
+ This tool is meant to be handed to an AI agent, so its safety story is enforced
65
+ by the server rather than requested in a prompt:
66
+
67
+ ```bash
68
+ export QUEUE_READ_ONLY=1
69
+ ```
70
+
71
+ With that set, the **8 write tools are never registered**. An MCP client
72
+ lists **20 tools instead of 28** — the writes are not hidden, not
73
+ gated behind a flag, and not merely refused when called. They are absent from
74
+ the session. A model cannot invoke a tool it was never offered, and cannot be
75
+ argued into one.
76
+
77
+ That distinction is the whole point. A tool that exists but refuses still invites
78
+ retry loops and "I'll describe the call instead" behaviour from smaller models,
79
+ and it leaves a reviewer trusting a promise. An absent tool is a fact you can
80
+ check: connect, list the tools, and see that the writes are not there.
81
+
82
+ Enforcement is two layers deep, so the switch cannot be sidestepped by changing
83
+ entry point:
84
+
85
+ | Layer | What it does | Covers |
86
+ |---|---|---|
87
+ | `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
88
+ | MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
89
+
90
+ Read operations are unaffected, and every call is still audited to
91
+ `~/.queue-aiops/audit.db`.
92
+
93
+ > The read/write split is derived from each tool's declared `risk_level`, and a
94
+ > test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
95
+ > tool's own documentation — so a write can't quietly present itself as a read.
96
+
97
+ Running a smaller / local model? See
98
+ [agent-guardrails.md](skills/queue-aiops/references/agent-guardrails.md) — it lists
99
+ the guardrails this tool now enforces for you (so you don't spend prompt budget
100
+ restating them) and gives a ready-made system prompt for what's left.
101
+
61
102
  ## Support scope
62
103
 
63
104
  | Platform | Protocol | Coverage |
@@ -65,7 +106,7 @@ queue-aiops rabbitmq queues # deepest backlog first
65
106
  | **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 |
66
107
  | **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 |
67
108
 
68
- **26 MCP tools** — 19 reads (incl. 4 flagship RCAs) + 7 governed writes.
109
+ **28 MCP tools** — 20 reads (incl. 4 flagship RCAs) + 8 governed writes.
69
110
 
70
111
  | Group | Tools | R/W |
71
112
  |-------|-------|:---:|
@@ -75,6 +116,7 @@ queue-aiops rabbitmq queues # deepest backlog first
75
116
  | Flagship RCAs | redis_memory_pressure_rca, redis_latency_rca, rabbitmq_queue_backlog_rca, connection_churn_analysis | read |
76
117
  | Writes (medium) | redis_config_set, redis_kill_client, declare_queue, set_policy, delete_policy | write |
77
118
  | Writes (**high**) | purge_queue, delete_queue | write |
119
+ | Undo | undo_list, undo_apply | read / write |
78
120
 
79
121
  The four RCAs are transparent heuristics that report their numbers — thresholds
80
122
  are named constants, every finding carries its evidence, never a black-box
@@ -150,12 +192,19 @@ queue-aiops analyze memory|latency|backlog|churn
150
192
 
151
193
  ## Verification status
152
194
 
153
- Preview / mock-only: the full test suite runs against mocked clients (no live
154
- broker needed), and the REST paths / INFO fields are modelled from the public
155
- docs of both platforms. Not yet validated against live production brokers —
156
- both are trivially self-hostable, so `queue-aiops doctor` against a
157
- local lab redis instance / rabbitmq broker (management plugin enabled) is
158
- the quickest live check.
195
+ **Live-verified against Redis 7.4.9 (2026-07-19); RabbitMQ still mock-only.**
196
+ Connectivity, every Redis read (cross-checked against `redis-cli` ground truth), all
197
+ four analyses, and the full governance loop (real `redis_config_set` → audit row →
198
+ undo restoring the prior value) were exercised against a real server. That run found
199
+ and fixed a real defect: integer quantities — key counts, client counts, byte totals —
200
+ were rendered as floats (`202.0` keys), which equality assertions cannot catch.
201
+
202
+ The **entire RabbitMQ command group** remains unverified against a live broker, as do
203
+ Redis cluster/sentinel topologies and AUTH/TLS connections.
204
+
205
+ [docs/VERIFICATION.md](docs/VERIFICATION.md) records exactly what was checked and what
206
+ is still open. `queue-aiops doctor` is the fastest live check.
207
+
159
208
 
160
209
  ## Contributing
161
210
 
@@ -1,6 +1,6 @@
1
1
  <!-- mcp-name: io.github.AIops-tools/queue-aiops -->
2
2
 
3
- # Queue AIops (preview)
3
+ # Queue AIops
4
4
 
5
5
  **Governed AI-ops for redis + rabbitmq.** queue-aiops is for the team running
6
6
  their own cache and message broker — a redis that "suddenly eats memory", a
@@ -16,9 +16,10 @@ by the built-in governance harness.
16
16
  > with, endorsed by, or sponsored by the Redis or RabbitMQ projects or their
17
17
  > respective owners.** Redis and RabbitMQ are trademarks of their respective owners.
18
18
  >
19
- > **Preview / mock-only**: not yet validated against production brokers. Both
20
- > redis and rabbitmq are free/self-hostable (one lab container or package install each), so a lab
21
- > check is easy — `queue-aiops doctor` is the fastest live probe.
19
+ > **Verification**: behaviour is covered by a mock-based test suite; not yet validated
20
+ > against live brokers. Both redis and rabbitmq are free/self-hostable (one lab container
21
+ > or package install each), so a lab check is easy — `queue-aiops doctor` is the fastest
22
+ > live probe, and [docs/VERIFICATION.md](docs/VERIFICATION.md) is the checklist.
22
23
 
23
24
  ## Quick start
24
25
 
@@ -41,6 +42,46 @@ queue-aiops redis bigkeys # SCAN-budgeted big-key sample (never KEYS *)
41
42
  queue-aiops rabbitmq queues # deepest backlog first
42
43
  ```
43
44
 
45
+ ## Security: read-only mode
46
+
47
+ This tool is meant to be handed to an AI agent, so its safety story is enforced
48
+ by the server rather than requested in a prompt:
49
+
50
+ ```bash
51
+ export QUEUE_READ_ONLY=1
52
+ ```
53
+
54
+ With that set, the **8 write tools are never registered**. An MCP client
55
+ lists **20 tools instead of 28** — the writes are not hidden, not
56
+ gated behind a flag, and not merely refused when called. They are absent from
57
+ the session. A model cannot invoke a tool it was never offered, and cannot be
58
+ argued into one.
59
+
60
+ That distinction is the whole point. A tool that exists but refuses still invites
61
+ retry loops and "I'll describe the call instead" behaviour from smaller models,
62
+ and it leaves a reviewer trusting a promise. An absent tool is a fact you can
63
+ check: connect, list the tools, and see that the writes are not there.
64
+
65
+ Enforcement is two layers deep, so the switch cannot be sidestepped by changing
66
+ entry point:
67
+
68
+ | Layer | What it does | Covers |
69
+ |---|---|---|
70
+ | `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
71
+ | MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
72
+
73
+ Read operations are unaffected, and every call is still audited to
74
+ `~/.queue-aiops/audit.db`.
75
+
76
+ > The read/write split is derived from each tool's declared `risk_level`, and a
77
+ > test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
78
+ > tool's own documentation — so a write can't quietly present itself as a read.
79
+
80
+ Running a smaller / local model? See
81
+ [agent-guardrails.md](skills/queue-aiops/references/agent-guardrails.md) — it lists
82
+ the guardrails this tool now enforces for you (so you don't spend prompt budget
83
+ restating them) and gives a ready-made system prompt for what's left.
84
+
44
85
  ## Support scope
45
86
 
46
87
  | Platform | Protocol | Coverage |
@@ -48,7 +89,7 @@ queue-aiops rabbitmq queues # deepest backlog first
48
89
  | **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 |
49
90
  | **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 |
50
91
 
51
- **26 MCP tools** — 19 reads (incl. 4 flagship RCAs) + 7 governed writes.
92
+ **28 MCP tools** — 20 reads (incl. 4 flagship RCAs) + 8 governed writes.
52
93
 
53
94
  | Group | Tools | R/W |
54
95
  |-------|-------|:---:|
@@ -58,6 +99,7 @@ queue-aiops rabbitmq queues # deepest backlog first
58
99
  | Flagship RCAs | redis_memory_pressure_rca, redis_latency_rca, rabbitmq_queue_backlog_rca, connection_churn_analysis | read |
59
100
  | Writes (medium) | redis_config_set, redis_kill_client, declare_queue, set_policy, delete_policy | write |
60
101
  | Writes (**high**) | purge_queue, delete_queue | write |
102
+ | Undo | undo_list, undo_apply | read / write |
61
103
 
62
104
  The four RCAs are transparent heuristics that report their numbers — thresholds
63
105
  are named constants, every finding carries its evidence, never a black-box
@@ -133,12 +175,19 @@ queue-aiops analyze memory|latency|backlog|churn
133
175
 
134
176
  ## Verification status
135
177
 
136
- Preview / mock-only: the full test suite runs against mocked clients (no live
137
- broker needed), and the REST paths / INFO fields are modelled from the public
138
- docs of both platforms. Not yet validated against live production brokers —
139
- both are trivially self-hostable, so `queue-aiops doctor` against a
140
- local lab redis instance / rabbitmq broker (management plugin enabled) is
141
- the quickest live check.
178
+ **Live-verified against Redis 7.4.9 (2026-07-19); RabbitMQ still mock-only.**
179
+ Connectivity, every Redis read (cross-checked against `redis-cli` ground truth), all
180
+ four analyses, and the full governance loop (real `redis_config_set` → audit row →
181
+ undo restoring the prior value) were exercised against a real server. That run found
182
+ and fixed a real defect: integer quantities — key counts, client counts, byte totals —
183
+ were rendered as floats (`202.0` keys), which equality assertions cannot catch.
184
+
185
+ The **entire RabbitMQ command group** remains unverified against a live broker, as do
186
+ Redis cluster/sentinel topologies and AUTH/TLS connections.
187
+
188
+ [docs/VERIFICATION.md](docs/VERIFICATION.md) records exactly what was checked and what
189
+ is still open. `queue-aiops doctor` is the fastest live check.
190
+
142
191
 
143
192
  ## Contributing
144
193
 
@@ -0,0 +1,26 @@
1
+ # Release notes — queue-aiops 0.2.1
2
+
3
+ Previous release: 0.2.0.
4
+
5
+ ## Fixed: integer quantities were rendered as floats
6
+
7
+ Key counts, client counts and byte totals were routed through the float coercion
8
+ helper, so a live server reported `202.0` keys and `1.0` connected clients. The
9
+ values were arithmetically right but semantically wrong — a count cannot be
10
+ fractional, and a reader should not have to wonder whether `.0` means the number was
11
+ rounded.
12
+
13
+ These now use a new `as_int()` helper and come back as integers. Genuinely fractional
14
+ values (`hitRatePct`, `usedPctOfMax`, `opsPerSec`, `expiresPct`) are unchanged. Note
15
+ that equality assertions cannot catch this class of bug (`202 == 202.0`), so the
16
+ regression test asserts the *type*.
17
+
18
+ If you parse these fields, the JSON shape changes from `202.0` to `202`.
19
+
20
+ ## Live-verified
21
+
22
+ This release was exercised end-to-end against a real **Redis 7.4.9** server: every
23
+ Redis read cross-checked against `redis-cli` ground truth, all four analyses, and the
24
+ full governance loop (real `redis_config_set` → audit row → `undo_apply` restoring the
25
+ prior value). See [docs/VERIFICATION.md](docs/VERIFICATION.md) — **RabbitMQ remains
26
+ mock-only** and is now the largest gap in this repo.
@@ -0,0 +1,34 @@
1
+ # Live verification status
2
+
3
+ This document records what has and has not been validated against real Redis /
4
+ RabbitMQ servers, so the maturity claim is auditable rather than a vibe.
5
+
6
+ ## Already live-verified ✅ — Redis 7.4.9, 2026-07-19
7
+
8
+ Exercised end-to-end against a real **Redis 7.4.9** server (Docker) seeded with
9
+ 202 keys and a list-backed job queue:
10
+
11
+ - `doctor` against a live server: PING OK, no-AUTH lab path reported clearly.
12
+ - Every Redis read cross-checked against `redis-cli` ground truth:
13
+ `overview`, `redis info/memory/clients/slowlog/keyspace/bigkeys`
14
+ (key count matched `DBSIZE` exactly).
15
+ - All four analyses ran clean: `analyze memory/latency/backlog/churn`.
16
+ - Governance loop end-to-end: `redis_config_set` really changed
17
+ `maxmemory-policy`, captured `noeviction` as `priorState`, wrote an audit row
18
+ with approver, and `undo_apply` restored the prior value on the live server.
19
+
20
+ **A real defect was found and fixed by this run**: integer quantities were
21
+ rendered as floats — `202.0` keys, `1.0` connected clients, float byte counts —
22
+ because they were routed through `num()`. Values were arithmetically right but
23
+ semantically wrong, and equality assertions could not catch it
24
+ (`202 == 202.0`). Fixed with `as_int()` across the Redis and RabbitMQ reads,
25
+ plus a regression test that asserts the *type*, keeping genuine ratios
26
+ (`hitRatePct`, `usedPctOfMax`, `opsPerSec`) as floats.
27
+
28
+ ## Not yet live-verified ⚠️
29
+
30
+ - **RabbitMQ** — the entire `rabbitmq` command group and its management-API
31
+ shapes are unit-tested only. This is now the largest gap in this repo.
32
+ - Redis **cluster / sentinel** topologies (only standalone was exercised).
33
+ - AUTH-enabled Redis and TLS connections.
34
+ - `kill-client` against a real blocked client.
@@ -75,7 +75,7 @@ def tool_errors(shape: str = "dict") -> Callable:
75
75
  mcp = FastMCP(
76
76
  "queue-aiops",
77
77
  instructions=(
78
- "Queue/cache broker operations (preview) over redis and rabbitmq: for a "
78
+ "Queue/cache broker operations over redis and rabbitmq: for a "
79
79
  "redis target — server/memory/keyspace reads, clients, slowlog, config, "
80
80
  "and a SCAN-budgeted big-key sample (never KEYS *); for a rabbitmq "
81
81
  "target — management-API overview, queues, connections, channels, "
@@ -0,0 +1,76 @@
1
+ """MCP server wrapping queue-aiops operations (stdio transport).
2
+
3
+ Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
4
+ delegates to the ``queue_aiops`` ops package and is wrapped with the
5
+ queue-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
6
+
7
+ Standalone, self-governed broker operations over redis and rabbitmq:
8
+ redis server/memory/clients/slowlog/config/keyspace/big-key reads, rabbitmq
9
+ overview/queues/connections/channels/policies/node reads, four flagship
10
+ analyses, and governed writes (redis config set + client kill; rabbitmq queue
11
+ purge/delete/declare and policy set/delete).
12
+
13
+ Source: https://github.com/AIops-tools/Queue-AIops
14
+ License: MIT
15
+ """
16
+
17
+ import logging
18
+
19
+ from mcp_server._shared import _safe_error, mcp, tool_errors
20
+
21
+ # Importing the tool modules registers every @mcp.tool() onto the shared
22
+ # `mcp` instance. Order does not matter; each module is self-contained.
23
+ from mcp_server.tools import ( # noqa: F401 — side effects
24
+ analysis,
25
+ overview,
26
+ rabbit_reads,
27
+ redis_reads,
28
+ undo,
29
+ writes,
30
+ )
31
+ from queue_aiops.governance import READ_ONLY_ENV, is_read_only
32
+
33
+ __all__ = ["mcp", "main", "_safe_error", "tool_errors", "apply_read_only"]
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ def apply_read_only() -> list[str]:
39
+ """Unregister every write tool when read-only mode is on.
40
+
41
+ The ``@governed_tool`` harness already refuses writes in this mode, so this
42
+ is a second layer — and the one that matters for smaller local models:
43
+ a tool absent from ``list_tools()`` cannot be hallucinated into a call,
44
+ whereas a tool that exists but refuses invites retry loops. It also gives a
45
+ compliance reviewer something checkable — the write tools are simply not
46
+ exposed.
47
+
48
+ ``risk_level == "low"`` is the read/write discriminator; a smoke test
49
+ asserts it stays in agreement with each tool's ``[READ]``/``[WRITE]``
50
+ docstring tag so the two can never drift apart silently.
51
+
52
+ Returns the names that were removed (empty when not in read-only mode).
53
+ """
54
+ if not is_read_only():
55
+ return []
56
+ registry = mcp._tool_manager._tools
57
+ dropped = [
58
+ name
59
+ for name, tool in registry.items()
60
+ if getattr(getattr(tool, "fn", None), "_risk_level", "low") != "low"
61
+ ]
62
+ for name in dropped:
63
+ del registry[name]
64
+ if dropped:
65
+ logger.info(
66
+ "%s is set — read-only mode: %d write tool(s) not exposed",
67
+ READ_ONLY_ENV, len(dropped),
68
+ )
69
+ return dropped
70
+
71
+
72
+ def main() -> None:
73
+ """Run the MCP server over stdio."""
74
+ logging.basicConfig(level=logging.INFO)
75
+ apply_read_only()
76
+ mcp.run(transport="stdio")
@@ -28,6 +28,10 @@ def list_queues(vhost: Optional[str] = None, target: Optional[str] = None) -> di
28
28
  Args:
29
29
  vhost: Restrict to one vhost (the default vhost is '/'); omit for all.
30
30
  target: rabbitmq target name from config; omit for the default.
31
+
32
+ Returns an envelope: {"queues": [...], "returned": N, "limit": L,
33
+ "truncated": bool}. When "truncated" is true there is more than was
34
+ returned — do not treat this as the complete set.
31
35
  """
32
36
  return ops.list_queues(_get_connection(target), vhost)
33
37
 
@@ -54,6 +58,10 @@ def list_connections(target: Optional[str] = None) -> dict:
54
58
 
55
59
  Args:
56
60
  target: rabbitmq target name from config; omit for the default.
61
+
62
+ Returns an envelope: {"connections": [...], "returned": N, "limit": L,
63
+ "truncated": bool}. When "truncated" is true there is more than was
64
+ returned — do not treat this as the complete set.
57
65
  """
58
66
  return ops.list_connections(_get_connection(target))
59
67
 
@@ -66,6 +74,10 @@ def list_channels(target: Optional[str] = None) -> dict:
66
74
 
67
75
  Args:
68
76
  target: rabbitmq target name from config; omit for the default.
77
+
78
+ Returns an envelope: {"channels": [...], "returned": N, "limit": L,
79
+ "truncated": bool}. When "truncated" is true there is more than was
80
+ returned — do not treat this as the complete set.
69
81
  """
70
82
  return ops.list_channels(_get_connection(target))
71
83
 
@@ -79,6 +91,10 @@ def list_policies(vhost: Optional[str] = None, target: Optional[str] = None) ->
79
91
  Args:
80
92
  vhost: Restrict to one vhost (the default vhost is '/'); omit for all.
81
93
  target: rabbitmq target name from config; omit for the default.
94
+
95
+ Returns an envelope: {"policies": [...], "returned": N, "limit": L,
96
+ "truncated": bool}. When "truncated" is true there is more than was
97
+ returned — do not treat this as the complete set.
82
98
  """
83
99
  return ops.list_policies(_get_connection(target), vhost)
84
100
 
@@ -39,6 +39,10 @@ def redis_clients(target: Optional[str] = None) -> dict:
39
39
 
40
40
  Args:
41
41
  target: redis target name from config; omit for the default.
42
+
43
+ Returns an envelope: {"clients": [...], "returned": N, "limit": L,
44
+ "truncated": bool}. When "truncated" is true there is more than was
45
+ returned — do not treat this as the complete set.
42
46
  """
43
47
  return ops.list_clients(_get_connection(target))
44
48
 
@@ -52,6 +56,10 @@ def redis_slowlog(count: int = 128, target: Optional[str] = None) -> dict:
52
56
  Args:
53
57
  count: Max entries to pull (capped at 128).
54
58
  target: redis target name from config; omit for the default.
59
+
60
+ Returns an envelope: {"entries": [...], "returned": N, "limit": L,
61
+ "truncated": bool}. When "truncated" is true there is more than was
62
+ returned — do not treat this as the complete set.
55
63
  """
56
64
  return ops.slowlog(_get_connection(target), count)
57
65
 
@@ -94,5 +102,9 @@ def redis_big_keys(top: int = 20, target: Optional[str] = None) -> dict:
94
102
  Args:
95
103
  top: How many key rows to return, largest first (default 20).
96
104
  target: redis target name from config; omit for the default.
105
+
106
+ Returns an envelope: {"topKeys": [...], "returned": N, "limit": L,
107
+ "truncated": bool}. When "truncated" is true there is more than was
108
+ returned — do not treat this as the complete set.
97
109
  """
98
110
  return ops.big_key_sample(_get_connection(target), top)
@@ -36,13 +36,24 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
36
36
  Each entry names the original tool, the inverse tool that ``undo_apply``
37
37
  would run, and a human note. Use the ``undoId`` with ``undo_apply``.
38
38
 
39
+ Returns {"undos": [...], "returned": N, "limit": L, "truncated": bool}.
40
+ ``truncated`` is *measured* (one extra row is fetched), not guessed from a
41
+ length coincidence: when it is true there are MORE tokens than shown, so
42
+ re-run with a higher ``limit`` rather than reporting the list as complete.
43
+
39
44
  Args:
40
- limit: Max rows to return (default 50).
45
+ limit: Max rows to return (default 50, capped at 500).
41
46
  target: Unused (undo state is host-local); accepted for CLI uniformity.
42
47
  """
43
- rows = get_undo_store().list(status="recorded", limit=max(1, min(limit, 500)))
48
+ requested = max(1, min(limit, 500))
49
+ raw = get_undo_store().list(status="recorded", limit=requested + 1)
50
+ truncated = len(raw) > requested
51
+ rows = raw[:requested]
44
52
  return {
45
53
  "count": len(rows),
54
+ "returned": len(rows),
55
+ "limit": requested,
56
+ "truncated": truncated,
46
57
  "undos": [
47
58
  {
48
59
  "undoId": r["undo_id"],
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "queue-aiops"
7
- version = "0.1.1"
7
+ version = "0.2.1"
8
8
  description = "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)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -51,7 +51,13 @@ def rabbit_queues(
51
51
  from queue_aiops.ops import rabbit_reads as ops
52
52
 
53
53
  conn, _ = get_connection(target)
54
- console.print_json(json.dumps(ops.list_queues(conn, vhost)))
54
+ result = ops.list_queues(conn, vhost)
55
+ console.print_json(json.dumps(result))
56
+ if result.get("truncated"):
57
+ console.print(
58
+ f"[yellow]… truncated at {result.get('limit')} rows — "
59
+ f"re-run with a higher limit to see the rest.[/yellow]"
60
+ )
55
61
 
56
62
 
57
63
  @rabbitmq_app.command("queue")
@@ -75,7 +81,13 @@ def rabbit_connections(target: TargetOption = None) -> None:
75
81
  from queue_aiops.ops import rabbit_reads as ops
76
82
 
77
83
  conn, _ = get_connection(target)
78
- console.print_json(json.dumps(ops.list_connections(conn)))
84
+ result = ops.list_connections(conn)
85
+ console.print_json(json.dumps(result))
86
+ if result.get("truncated"):
87
+ console.print(
88
+ f"[yellow]… truncated at {result.get('limit')} rows — "
89
+ f"re-run with a higher limit to see the rest.[/yellow]"
90
+ )
79
91
 
80
92
 
81
93
  @rabbitmq_app.command("channels")
@@ -85,7 +97,13 @@ def rabbit_channels(target: TargetOption = None) -> None:
85
97
  from queue_aiops.ops import rabbit_reads as ops
86
98
 
87
99
  conn, _ = get_connection(target)
88
- console.print_json(json.dumps(ops.list_channels(conn)))
100
+ result = ops.list_channels(conn)
101
+ console.print_json(json.dumps(result))
102
+ if result.get("truncated"):
103
+ console.print(
104
+ f"[yellow]… truncated at {result.get('limit')} rows — "
105
+ f"re-run with a higher limit to see the rest.[/yellow]"
106
+ )
89
107
 
90
108
 
91
109
  @rabbitmq_app.command("policies")
@@ -100,7 +118,13 @@ def rabbit_policies(
100
118
  from queue_aiops.ops import rabbit_reads as ops
101
119
 
102
120
  conn, _ = get_connection(target)
103
- console.print_json(json.dumps(ops.list_policies(conn, vhost)))
121
+ result = ops.list_policies(conn, vhost)
122
+ console.print_json(json.dumps(result))
123
+ if result.get("truncated"):
124
+ console.print(
125
+ f"[yellow]… truncated at {result.get('limit')} rows — "
126
+ f"re-run with a higher limit to see the rest.[/yellow]"
127
+ )
104
128
 
105
129
 
106
130
  @rabbitmq_app.command("nodes")
@@ -52,7 +52,13 @@ def redis_clients(target: TargetOption = None) -> None:
52
52
  from queue_aiops.ops import redis_reads as ops
53
53
 
54
54
  conn, _ = get_connection(target)
55
- console.print_json(json.dumps(ops.list_clients(conn)))
55
+ result = ops.list_clients(conn)
56
+ console.print_json(json.dumps(result))
57
+ if result.get("truncated"):
58
+ console.print(
59
+ f"[yellow]… truncated at {result.get('limit')} rows — "
60
+ f"only the busiest clients are shown.[/yellow]"
61
+ )
56
62
 
57
63
 
58
64
  @redis_app.command("slowlog")
@@ -65,7 +71,13 @@ def redis_slowlog(
65
71
  from queue_aiops.ops import redis_reads as ops
66
72
 
67
73
  conn, _ = get_connection(target)
68
- console.print_json(json.dumps(ops.slowlog(conn, count)))
74
+ result = ops.slowlog(conn, count)
75
+ console.print_json(json.dumps(result))
76
+ if result.get("truncated"):
77
+ console.print(
78
+ f"[yellow]… truncated at {result.get('limit')} rows — "
79
+ f"re-run with a higher --count to see the rest.[/yellow]"
80
+ )
69
81
 
70
82
 
71
83
  @redis_app.command("config-get")
@@ -18,12 +18,16 @@ from queue_aiops.governance.budget import BudgetExceeded, BudgetTracker, get_bud
18
18
  from queue_aiops.governance.decorators import PolicyDenied, governed_tool
19
19
  from queue_aiops.governance.patterns import Pattern, PatternMatch, get_pattern_engine
20
20
  from queue_aiops.governance.policy import TierDecision, get_policy_engine
21
- from queue_aiops.governance.sanitize import sanitize
21
+ from queue_aiops.governance.readonly import READ_ONLY_ENV, is_read_only
22
+ from queue_aiops.governance.sanitize import opt_str, sanitize
22
23
  from queue_aiops.governance.undo import UndoStore, get_undo_store
23
24
 
24
25
  __all__ = [
25
26
  "governed_tool",
26
27
  "sanitize",
28
+ "opt_str",
29
+ "is_read_only",
30
+ "READ_ONLY_ENV",
27
31
  "PolicyDenied",
28
32
  "get_engine",
29
33
  "AuditEngine",
@@ -36,6 +36,7 @@ from queue_aiops.governance.audit import detect_agent, get_engine
36
36
  from queue_aiops.governance.budget import BudgetExceeded, get_budget
37
37
  from queue_aiops.governance.patterns import PatternMatch, get_pattern_engine
38
38
  from queue_aiops.governance.policy import PolicyResult, get_policy_engine
39
+ from queue_aiops.governance.readonly import is_read_only, read_only_denial
39
40
  from queue_aiops.governance.sanitize import sanitize
40
41
 
41
42
  _log = logging.getLogger("queue-aiops.decorators")
@@ -233,6 +234,17 @@ def _pre_check(state: _CallState) -> None:
233
234
  never block the call (fail-open by design — a broken pattern file must
234
235
  not take down every MCP tool).
235
236
  """
237
+ # Read-only mode is a hard switch, checked before policy so that no write
238
+ # reaches an API regardless of entry point (MCP, CLI, or in-process) and
239
+ # regardless of what rules.yaml says. risk_level 'low' == a read.
240
+ if state.risk_level != "low" and is_read_only():
241
+ reason = read_only_denial(state.tool_name)
242
+ denial = PolicyResult(allowed=False, rule="read_only", reason=reason)
243
+ state.policy_result = denial
244
+ state.status = "denied"
245
+ state.result = {"error": reason, "rule": denial.rule}
246
+ raise PolicyDenied(denial)
247
+
236
248
  state.policy_result = state.policy.check_allowed(
237
249
  state.tool_name,
238
250
  env=state.env,