queue-aiops 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/CHANGELOG.md +20 -0
  2. queue_aiops-0.5.0/Dockerfile +9 -0
  3. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/PKG-INFO +35 -48
  4. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/README.md +33 -46
  5. queue_aiops-0.5.0/RELEASE_NOTES.md +29 -0
  6. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/SECURITY.md +6 -5
  7. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/docs/VERIFICATION.md +2 -2
  8. queue_aiops-0.5.0/glama.json +6 -0
  9. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/_shared.py +12 -8
  10. queue_aiops-0.5.0/mcp_server/server.py +51 -0
  11. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/undo.py +41 -12
  12. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/writes.py +4 -7
  13. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/pyproject.toml +2 -2
  14. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/__init__.py +1 -1
  15. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/_common.py +10 -2
  16. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/init.py +0 -38
  17. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/rabbit_cmds.py +37 -14
  18. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/undo.py +11 -5
  19. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/config.py +14 -2
  20. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/__init__.py +1 -5
  21. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/budget.py +19 -22
  22. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/decorators.py +61 -101
  23. queue_aiops-0.5.0/queue_aiops/governance/policy.py +67 -0
  24. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/_util.py +4 -0
  25. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/server.json +2 -2
  26. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/skills/queue-aiops/SKILL.md +23 -15
  27. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/agent-guardrails.md +29 -13
  28. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/capabilities.md +5 -6
  29. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/cli-reference.md +3 -2
  30. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/setup-guide.md +5 -6
  31. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/conftest.py +3 -4
  32. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_cli_rabbit_writes.py +43 -3
  33. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_cli_writes.py +38 -5
  34. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_gov_decorators.py +11 -28
  35. queue_aiops-0.5.0/tests/test_gov_policy.py +56 -0
  36. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_governance_persistence.py +23 -36
  37. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_init.py +5 -16
  38. queue_aiops-0.5.0/tests/test_int_fidelity.py +45 -0
  39. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_smoke.py +30 -0
  40. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_undo_executor.py +59 -1
  41. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/uv.lock +66 -26
  42. queue_aiops-0.3.0/RELEASE_NOTES.md +0 -50
  43. queue_aiops-0.3.0/mcp_server/server.py +0 -76
  44. queue_aiops-0.3.0/queue_aiops/governance/policy.py +0 -430
  45. queue_aiops-0.3.0/queue_aiops/governance/readonly.py +0 -49
  46. queue_aiops-0.3.0/tests/test_gov_policy.py +0 -415
  47. queue_aiops-0.3.0/tests/test_readonly.py +0 -142
  48. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/.github/workflows/mcp-publish.yml +0 -0
  49. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/.github/workflows/publish.yml +0 -0
  50. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/.gitignore +0 -0
  51. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/LICENSE +0 -0
  52. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/__init__.py +0 -0
  53. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/__init__.py +0 -0
  54. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/analysis.py +0 -0
  55. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/overview.py +0 -0
  56. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/rabbit_reads.py +0 -0
  57. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/mcp_server/tools/redis_reads.py +0 -0
  58. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/__init__.py +0 -0
  59. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/_root.py +0 -0
  60. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/analyze.py +0 -0
  61. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/doctor.py +0 -0
  62. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/overview.py +0 -0
  63. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/redis_cmds.py +0 -0
  64. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/cli/secret.py +0 -0
  65. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/connection.py +0 -0
  66. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/doctor.py +0 -0
  67. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/audit.py +0 -0
  68. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/outcome.py +0 -0
  69. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/paths.py +0 -0
  70. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/patterns.py +0 -0
  71. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/sanitize.py +0 -0
  72. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/governance/undo.py +0 -0
  73. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/__init__.py +0 -0
  74. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/analysis.py +0 -0
  75. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/overview.py +0 -0
  76. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/rabbit_reads.py +0 -0
  77. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/redis_reads.py +0 -0
  78. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/ops/writes.py +0 -0
  79. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/platform.py +0 -0
  80. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/queue_aiops/secretstore.py +0 -0
  81. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/smithery.yaml +0 -0
  82. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/fakes.py +0 -0
  83. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_analysis.py +0 -0
  84. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_cli_reads.py +0 -0
  85. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_cli_secret.py +0 -0
  86. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_connection.py +0 -0
  87. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_doctor.py +0 -0
  88. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_gov_audit.py +0 -0
  89. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_gov_patterns.py +0 -0
  90. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_mcp_reads.py +0 -0
  91. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_optional_fields.py +0 -0
  92. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_platform.py +0 -0
  93. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_reads.py +0 -0
  94. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_secretstore.py +0 -0
  95. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_self_kill.py +0 -0
  96. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_self_lockout.py +0 -0
  97. {queue_aiops-0.3.0 → queue_aiops-0.5.0}/tests/test_writes.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.0 — 2026-08-02
4
+
5
+ ### Changed (BREAKING)
6
+ - **Requires MCP SDK 2.0** (`mcp[cli]>=2.0,<3.0`). `mcp.server.fastmcp` no longer exists in 2.0; the server is now built with `MCPServer` and reports its package version in the stdio handshake.
7
+
8
+ ### Fixed
9
+ - **`undo apply` works from the CLI.** Every write tool is imported lazily inside its own CLI command, so a CLI-driven undo ran in a process where the inverse tool was never registered and failed with "inverse tool is not registered" — for every write tool. Only the MCP entry point, which imports the whole server, worked. Found while live-verifying against a real cluster.
10
+ - **An undetermined outcome is audited `unknown`, not `ok`.** The harness only classified a result as undetermined when the payload *also* carried an `error` key, so a write that looked successful but had not been confirmed was recorded as a success.
11
+ - **`as_int` no longer round-trips integers through float64**, which cannot represent values above 2**53 exactly. A line-wide sweep found only one of six vendored copies had actually been fixed after the original precision bug. The bool guard precedes the int short-circuit because `bool` subclasses `int` — otherwise `True` would be returned unchanged and serialised as `true` rather than a number.
12
+
13
+
14
+ ## v0.4.0 — 2026-07-21
15
+
16
+ ### Changed (BREAKING)
17
+ - **Removed the authorization layer** — read-only mode, the approver gate, and rules.yaml deny are gone. The skill no longer decides read vs write; that is the agent's judgement or the connecting account's permissions. `<PREFIX>_READ_ONLY` now has no effect (a startup warning is logged); `<PREFIX>_AUDIT_APPROVED_BY`/`_RATIONALE` are optional audit annotations.
18
+ - The retained guarantee is **unbypassable audit over MCP and CLI alike** — no unaudited entry point. Harness = audit + runaway safety guard + undo + sanitize; `risk_level` is a descriptive audit label, not a gate.
19
+
20
+ See RELEASE_NOTES.md for tool-specific changes.
21
+
22
+
3
23
  ## v0.3.0 — 2026-07-20
4
24
 
5
25
  ### Fixed
@@ -0,0 +1,9 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Minimal image for Glama introspection: starts the MCP server over stdio.
3
+ # The tools/list introspection handshake needs no live Redis/RabbitMQ credentials.
4
+ FROM python:3.12-slim
5
+
6
+ RUN pip install --no-cache-dir queue-aiops
7
+
8
+ # MCP server speaks JSON-RPC over stdio.
9
+ ENTRYPOINT ["queue-aiops-mcp"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queue-aiops
3
- Version: 0.3.0
3
+ Version: 0.5.0
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 Zhou <zhouwei008@gmail.com>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
9
  Requires-Dist: cryptography>=42.0
10
10
  Requires-Dist: httpx<1.0,>=0.27
11
- Requires-Dist: mcp[cli]<2.0,>=1.10
11
+ Requires-Dist: mcp[cli]<3.0,>=2.0
12
12
  Requires-Dist: pyyaml<7.0,>=6.0
13
13
  Requires-Dist: redis<7.0,>=5.0
14
14
  Requires-Dist: rich<16.0,>=13.0
@@ -59,40 +59,24 @@ queue-aiops redis bigkeys # SCAN-budgeted big-key sample (never KEYS *)
59
59
  queue-aiops rabbitmq queues # deepest backlog first
60
60
  ```
61
61
 
62
- ## Security: read-only mode
62
+ ## What this tool does, and does not, decide
63
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:
64
+ It delivers broker operations — reads and writes — accurately and efficiently,
65
+ and records every one of them. It does **not** decide whether a write is allowed
66
+ to happen. That is the agent's judgement, or the permission of the account you
67
+ connect it with: give the Redis connection an ACL user restricted to read
68
+ commands, or the RabbitMQ management user only the `monitoring` tag, and the
69
+ writes fail at the broker — the place that actually owns the permission.
66
70
 
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`.
71
+ So there is no read-only switch, no policy file, no approval gate to configure.
72
+ The one thing the tool guarantees is that nothing is silent: **every call, over
73
+ MCP and over the CLI alike, lands an audit row** in `~/.queue-aiops/audit.db`,
74
+ and reversible writes still capture their before-state and record an inverse.
92
75
 
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.
76
+ > Each tool declares a `risk_level`, kept in agreement with its `[READ]`/`[WRITE]`
77
+ > documentation tag by a test, and carried into the audit row as a descriptive
78
+ > tier — so a reviewer can see at a glance that a row was a high-risk delete. It
79
+ > is a label, not a gate.
96
80
 
97
81
  Running a smaller / local model? See
98
82
  [agent-guardrails.md](skills/queue-aiops/references/agent-guardrails.md) — it lists
@@ -125,19 +109,21 @@ verdict. Big-key sampling walks at most 10,000 keys with SCAN and sizes at most
125
109
 
126
110
  ## Governance
127
111
 
128
- Every MCP tool runs through the bundled `@governed_tool` harness
129
- (`queue_aiops.governance` — no external dependency):
112
+ Every MCP tool — and every CLI write, which routes through the same governed
113
+ functions — runs through the bundled `@governed_tool` harness
114
+ (`queue_aiops.governance` — no external dependency). It **records; it does not
115
+ authorize** whether a write may happen (see above):
130
116
 
131
117
  - **Audit** — every call lands in `~/.queue-aiops/audit.db` (relocatable via
132
- `QUEUE_AIOPS_HOME`), secret-redacted.
133
- - **Budget** — call/time ceilings (`QUEUE_MAX_TOOL_CALLS`,
134
- `QUEUE_MAX_TOOL_SECONDS`) + a runaway-loop breaker.
135
- - **Risk tiers & approval** — **secure by default**: with no
136
- `~/.queue-aiops/rules.yaml`, high-risk writes (`purge_queue`,
137
- `delete_queue`) are denied unless `QUEUE_AUDIT_APPROVED_BY` names an approver
138
- (set `QUEUE_AUDIT_RATIONALE` too). `queue-aiops init` seeds a starter
139
- rules.yaml with that dual-control tier; an operator-authored rules file is
140
- honoured as-is.
118
+ `QUEUE_AIOPS_HOME`), secret-redacted. The CLI writes the same row the MCP path
119
+ does — there is no unaudited entry point.
120
+ - **Budget / runaway guard** — a safety backstop, not an authorization gate:
121
+ call/time ceilings (`QUEUE_MAX_TOOL_CALLS`, `QUEUE_MAX_TOOL_SECONDS`) + a
122
+ runaway-loop breaker stop a stuck agent from burning unbounded calls/time.
123
+ - **Risk tier** — a descriptive label on the audit row derived from `risk_level`;
124
+ it gates nothing. `QUEUE_AUDIT_APPROVED_BY` / `QUEUE_AUDIT_RATIONALE` are
125
+ optional annotations recorded on the row (who/why), never required and never
126
+ blocking.
141
127
  - **Undo** — reversible writes capture the real before-state first:
142
128
  `redis_config_set` records the prior value from CONFIG GET;
143
129
  `set_policy`/`delete_policy` record the prior policy; `delete_queue` records
@@ -146,7 +132,8 @@ Every MCP tool runs through the bundled `@governed_tool` harness
146
132
  `redis_kill_client`) record priorState only.
147
133
  - **Dry-run + double-confirm** — every write takes `dry_run=True` (MCP) /
148
134
  `--dry-run` (CLI); CLI writes double-confirm and execute through the same
149
- governed twins, so they land in the audit log too.
135
+ governed twins, so they land in the audit log too. `purge_queue` and
136
+ `delete_queue` are risk=high with dry-run + double confirmation at the CLI.
150
137
  - Credentials live **encrypted** in `~/.queue-aiops/secrets.enc` (Fernet +
151
138
  scrypt master password; `QUEUE_AIOPS_MASTER_PASSWORD` for non-interactive
152
139
  use). Redis passwords are optional — an auth-less lab instance is a
@@ -170,9 +157,9 @@ Every MCP tool runs through the bundled `@governed_tool` harness
170
157
 
171
158
  > **env-block caveat**: MCP clients launch the server with a *minimal*
172
159
  > environment — your shell profile is not sourced. Anything the server needs
173
- > (`QUEUE_AIOPS_MASTER_PASSWORD`, a relocated `QUEUE_AIOPS_HOME`,
174
- > `QUEUE_AUDIT_APPROVED_BY` for high-risk writes) must be set in the `env`
175
- > block above, not just in your terminal.
160
+ > (`QUEUE_AIOPS_MASTER_PASSWORD`, a relocated `QUEUE_AIOPS_HOME`, and any
161
+ > optional `QUEUE_AUDIT_APPROVED_BY`/`QUEUE_AUDIT_RATIONALE` audit annotations)
162
+ > must be set in the `env` block above, not just in your terminal.
176
163
 
177
164
  Or, with the package installed: `queue-aiops mcp`.
178
165
 
@@ -42,40 +42,24 @@ queue-aiops redis bigkeys # SCAN-budgeted big-key sample (never KEYS *)
42
42
  queue-aiops rabbitmq queues # deepest backlog first
43
43
  ```
44
44
 
45
- ## Security: read-only mode
45
+ ## What this tool does, and does not, decide
46
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:
47
+ It delivers broker operations — reads and writes — accurately and efficiently,
48
+ and records every one of them. It does **not** decide whether a write is allowed
49
+ to happen. That is the agent's judgement, or the permission of the account you
50
+ connect it with: give the Redis connection an ACL user restricted to read
51
+ commands, or the RabbitMQ management user only the `monitoring` tag, and the
52
+ writes fail at the broker — the place that actually owns the permission.
49
53
 
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`.
54
+ So there is no read-only switch, no policy file, no approval gate to configure.
55
+ The one thing the tool guarantees is that nothing is silent: **every call, over
56
+ MCP and over the CLI alike, lands an audit row** in `~/.queue-aiops/audit.db`,
57
+ and reversible writes still capture their before-state and record an inverse.
75
58
 
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.
59
+ > Each tool declares a `risk_level`, kept in agreement with its `[READ]`/`[WRITE]`
60
+ > documentation tag by a test, and carried into the audit row as a descriptive
61
+ > tier — so a reviewer can see at a glance that a row was a high-risk delete. It
62
+ > is a label, not a gate.
79
63
 
80
64
  Running a smaller / local model? See
81
65
  [agent-guardrails.md](skills/queue-aiops/references/agent-guardrails.md) — it lists
@@ -108,19 +92,21 @@ verdict. Big-key sampling walks at most 10,000 keys with SCAN and sizes at most
108
92
 
109
93
  ## Governance
110
94
 
111
- Every MCP tool runs through the bundled `@governed_tool` harness
112
- (`queue_aiops.governance` — no external dependency):
95
+ Every MCP tool — and every CLI write, which routes through the same governed
96
+ functions — runs through the bundled `@governed_tool` harness
97
+ (`queue_aiops.governance` — no external dependency). It **records; it does not
98
+ authorize** whether a write may happen (see above):
113
99
 
114
100
  - **Audit** — every call lands in `~/.queue-aiops/audit.db` (relocatable via
115
- `QUEUE_AIOPS_HOME`), secret-redacted.
116
- - **Budget** — call/time ceilings (`QUEUE_MAX_TOOL_CALLS`,
117
- `QUEUE_MAX_TOOL_SECONDS`) + a runaway-loop breaker.
118
- - **Risk tiers & approval** — **secure by default**: with no
119
- `~/.queue-aiops/rules.yaml`, high-risk writes (`purge_queue`,
120
- `delete_queue`) are denied unless `QUEUE_AUDIT_APPROVED_BY` names an approver
121
- (set `QUEUE_AUDIT_RATIONALE` too). `queue-aiops init` seeds a starter
122
- rules.yaml with that dual-control tier; an operator-authored rules file is
123
- honoured as-is.
101
+ `QUEUE_AIOPS_HOME`), secret-redacted. The CLI writes the same row the MCP path
102
+ does — there is no unaudited entry point.
103
+ - **Budget / runaway guard** — a safety backstop, not an authorization gate:
104
+ call/time ceilings (`QUEUE_MAX_TOOL_CALLS`, `QUEUE_MAX_TOOL_SECONDS`) + a
105
+ runaway-loop breaker stop a stuck agent from burning unbounded calls/time.
106
+ - **Risk tier** — a descriptive label on the audit row derived from `risk_level`;
107
+ it gates nothing. `QUEUE_AUDIT_APPROVED_BY` / `QUEUE_AUDIT_RATIONALE` are
108
+ optional annotations recorded on the row (who/why), never required and never
109
+ blocking.
124
110
  - **Undo** — reversible writes capture the real before-state first:
125
111
  `redis_config_set` records the prior value from CONFIG GET;
126
112
  `set_policy`/`delete_policy` record the prior policy; `delete_queue` records
@@ -129,7 +115,8 @@ Every MCP tool runs through the bundled `@governed_tool` harness
129
115
  `redis_kill_client`) record priorState only.
130
116
  - **Dry-run + double-confirm** — every write takes `dry_run=True` (MCP) /
131
117
  `--dry-run` (CLI); CLI writes double-confirm and execute through the same
132
- governed twins, so they land in the audit log too.
118
+ governed twins, so they land in the audit log too. `purge_queue` and
119
+ `delete_queue` are risk=high with dry-run + double confirmation at the CLI.
133
120
  - Credentials live **encrypted** in `~/.queue-aiops/secrets.enc` (Fernet +
134
121
  scrypt master password; `QUEUE_AIOPS_MASTER_PASSWORD` for non-interactive
135
122
  use). Redis passwords are optional — an auth-less lab instance is a
@@ -153,9 +140,9 @@ Every MCP tool runs through the bundled `@governed_tool` harness
153
140
 
154
141
  > **env-block caveat**: MCP clients launch the server with a *minimal*
155
142
  > environment — your shell profile is not sourced. Anything the server needs
156
- > (`QUEUE_AIOPS_MASTER_PASSWORD`, a relocated `QUEUE_AIOPS_HOME`,
157
- > `QUEUE_AUDIT_APPROVED_BY` for high-risk writes) must be set in the `env`
158
- > block above, not just in your terminal.
143
+ > (`QUEUE_AIOPS_MASTER_PASSWORD`, a relocated `QUEUE_AIOPS_HOME`, and any
144
+ > optional `QUEUE_AUDIT_APPROVED_BY`/`QUEUE_AUDIT_RATIONALE` audit annotations)
145
+ > must be set in the `env` block above, not just in your terminal.
159
146
 
160
147
  Or, with the package installed: `queue-aiops mcp`.
161
148
 
@@ -0,0 +1,29 @@
1
+ # Release notes — queue-aiops 0.4.0
2
+
3
+ Previous release: 0.3.0.
4
+
5
+ ## BREAKING — the authorization layer is removed
6
+
7
+ This tool no longer decides whether a write is permitted. Read-only mode
8
+ (`<PREFIX>_READ_ONLY`), the graduated-approval / approver gate, and the
9
+ `rules.yaml` deny engine are **all gone**. Whether an operation runs is the
10
+ agent's judgement, or the permission of the account you connect it with — point
11
+ it at a read-only credential and the write fails at the server, the place that
12
+ actually owns the permission.
13
+
14
+ What the tool guarantees instead is that **nothing is silent**: every operation,
15
+ over MCP **and** the CLI alike, lands a row in the audit log — there is no
16
+ unaudited entry point. Destructive writes still capture their before-state and
17
+ record an undo token where a clean inverse exists.
18
+
19
+ - If you set `<PREFIX>_READ_ONLY=1`, it now has **no effect** and the MCP server
20
+ logs a warning at startup. Restrict writes via the connecting account instead.
21
+ - `<PREFIX>_AUDIT_APPROVED_BY` / `<PREFIX>_AUDIT_RATIONALE` still work, but are now
22
+ **optional audit annotations** — recorded on the row when set, never required.
23
+ - The declared `risk_level` is carried into the audit row as a descriptive tier
24
+ (a label, not a gate).
25
+
26
+ The governance harness is now: **audit (MCP+CLI, unbypassable) · runaway/budget
27
+ safety guard · undo recording · output sanitize**. `policy.py` is a small
28
+ risk-tier classifier; `governance/readonly.py` is deleted.
29
+
@@ -40,8 +40,10 @@ Every MCP tool runs through the bundled `@governed_tool` harness
40
40
  - **Token/runaway budget** — hard ceilings (`QUEUE_MAX_TOOL_CALLS` /
41
41
  `QUEUE_MAX_TOOL_SECONDS`) plus an on-by-default guard that trips a tight
42
42
  poll/retry loop, preventing unbounded API consumption.
43
- - **Graduated risk tiers** — `~/.queue-aiops/rules.yaml` `risk_tiers` gate
44
- writes by environment/tag; the highest tiers require a recorded approver.
43
+ - **Risk tier** — a descriptive label on each audit row derived from
44
+ `risk_level`; it gates nothing. `QUEUE_AUDIT_APPROVED_BY` /
45
+ `QUEUE_AUDIT_RATIONALE` are optional annotations recorded on the row, never
46
+ required and never blocking.
45
47
  - **Undo-token recording** — reversible writes capture the BEFORE state (via a
46
48
  real read) and record an inverse descriptor (e.g. `redis_config_set` restores
47
49
  the prior value from CONFIG GET; `delete_queue` re-declares the captured
@@ -50,9 +52,8 @@ Every MCP tool runs through the bundled `@governed_tool` harness
50
52
 
51
53
  ### State-Changing Operations
52
54
  The destructive writes — `purge_queue` and `delete_queue` — are
53
- `risk_level=high`, accept a `dry_run` preview, and (under `risk_tiers`) require
54
- a recorded approver (`QUEUE_AUDIT_APPROVED_BY` + `QUEUE_AUDIT_RATIONALE`).
55
- Purged/deleted **messages cannot be restored** — `purge_queue` records the
55
+ `risk_level=high`, accept a `dry_run` preview, and require double confirmation
56
+ at the CLI. Purged/deleted **messages cannot be restored** — `purge_queue` records the
56
57
  prior message count as audit evidence (no undo) and `delete_queue`'s undo
57
58
  restores the queue *definition* only. `redis_config_set`, `redis_kill_client`
58
59
  (priorState = the client's CLIENT LIST row; no undo), `declare_queue`,
@@ -14,8 +14,8 @@ Exercised end-to-end against a real **Redis 7.4.9** server (Docker) seeded with
14
14
  (key count matched `DBSIZE` exactly).
15
15
  - All four analyses ran clean: `analyze memory/latency/backlog/churn`.
16
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.
17
+ `maxmemory-policy`, captured `noeviction` as `priorState`, wrote an audit row,
18
+ and `undo_apply` restored the prior value on the live server.
19
19
 
20
20
  **A real defect was found and fixed by this run**: integer quantities were
21
21
  rendered as floats — `202.0` keys, `1.0` connected clients, float byte counts —
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "zw008"
5
+ ]
6
+ }
@@ -1,13 +1,15 @@
1
- """Shared MCP server primitives: the FastMCP instance, connection helper,
1
+ """Shared MCP server primitives: the MCPServer instance, connection helper,
2
2
  error sanitisation, and the ``@tool_errors`` decorator.
3
3
 
4
4
  Tool modules under ``mcp_server/tools/`` import ``mcp`` from here and register
5
5
  their ``@mcp.tool()`` functions onto it. ``mcp_server/server.py`` then imports
6
6
  those modules and runs the server.
7
7
 
8
- Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any FastMCP-reflected
9
- tool signature — on older mcp/pydantic the union eval'd to ``types.UnionType``
10
- crashes FastMCP's ``issubclass`` check.
8
+ Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any reflected tool
9
+ signature. mcp>=2.0 accepts the union, but the legacy form is retained for
10
+ consistency and to stay safe under any pydantic reflection path (踩坑 #33 —
11
+ older mcp/pydantic eval'd the union to ``types.UnionType`` and crashed the
12
+ ``issubclass`` check).
11
13
  """
12
14
 
13
15
  import functools
@@ -19,8 +21,9 @@ from typing import Any, Optional
19
21
 
20
22
  import httpx
21
23
  import redis
22
- from mcp.server.fastmcp import FastMCP
24
+ from mcp.server import MCPServer
23
25
 
26
+ from queue_aiops import __version__
24
27
  from queue_aiops.config import load_config
25
28
  from queue_aiops.connection import ConnectionManager, QueueApiError
26
29
  from queue_aiops.governance import mark_unknown, sanitize
@@ -78,7 +81,7 @@ def tool_errors(shape: str = "dict") -> Callable:
78
81
  """Wrap a tool body in the canonical try/except → ``_safe_error`` pattern.
79
82
 
80
83
  Place this *between* ``@governed_tool`` and the function so the audit
81
- decorator and FastMCP still see the original signature.
84
+ decorator and MCPServer still see the original signature.
82
85
  """
83
86
 
84
87
  def decorator(func: Callable) -> Callable:
@@ -107,8 +110,9 @@ def tool_errors(shape: str = "dict") -> Callable:
107
110
  return decorator
108
111
 
109
112
 
110
- mcp = FastMCP(
113
+ mcp = MCPServer(
111
114
  "queue-aiops",
115
+ version=__version__,
112
116
  instructions=(
113
117
  "Queue/cache broker operations over redis and rabbitmq: for a "
114
118
  "redis target — server/memory/keyspace reads, clients, slowlog, config, "
@@ -120,7 +124,7 @@ mcp = FastMCP(
120
124
  "heuristics that show their numbers. Governed writes — config_set and "
121
125
  "set_policy/delete_policy (reversible, undo-recorded), declare_queue, "
122
126
  "kill_client (priorState only), plus purge_queue / delete_queue at "
123
- "risk=high with a dry_run preview and an approver (delete_queue's undo "
127
+ "risk=high with a dry_run preview (delete_queue's undo "
124
128
  "re-declares the captured definition; messages are not restored). Every "
125
129
  "tool runs through the queue-aiops governance harness (audit / budget / "
126
130
  "risk-tier / undo). A per-target 'platform' field selects the protocol "
@@ -0,0 +1,51 @@
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
+ import os
19
+
20
+ from mcp_server._shared import _safe_error, mcp, tool_errors
21
+
22
+ # Importing the tool modules registers every @mcp.tool() onto the shared
23
+ # `mcp` instance. Order does not matter; each module is self-contained.
24
+ from mcp_server.tools import ( # noqa: F401 — side effects
25
+ analysis,
26
+ overview,
27
+ rabbit_reads,
28
+ redis_reads,
29
+ undo,
30
+ writes,
31
+ )
32
+
33
+ __all__ = ["mcp", "main", "_safe_error", "tool_errors"]
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+
39
+ def main() -> None:
40
+ """Run the MCP server over stdio."""
41
+ logging.basicConfig(level=logging.INFO)
42
+ # Read-only mode was removed. Warn a deployment that still exports the old
43
+ # switch so it gets one audible signal instead of silently gaining writes.
44
+ if os.environ.get("QUEUE_READ_ONLY"):
45
+ logger.warning(
46
+ "QUEUE_READ_ONLY is set but no longer has any effect — "
47
+ "read-only mode was removed. Writes ARE enabled. Restrict them via "
48
+ "the connecting account's permissions instead (a read-only socket / "
49
+ "scope-limited token)."
50
+ )
51
+ mcp.run(transport="stdio")
@@ -3,10 +3,10 @@
3
3
  Every governed write records a replayable inverse descriptor (tool + params) to
4
4
  ``undo.db``. This module closes the loop: ``undo_apply`` looks up a recorded
5
5
  descriptor and dispatches it to the named governed tool, so the inverse runs on
6
- the SAME governance path as any other call (audited, budget-checked, and — if
7
- the inverse itself is destructive — re-gated by its own risk tier / approver
8
- requirement). ``undo_apply`` is itself governed; the real risk is enforced by
9
- the inner tool it calls.
6
+ the SAME governance path as any other call (audited and budget-checked, and — if
7
+ the inverse itself is destructive — recorded under its own risk tier). Whether
8
+ the inverse *should* run is the caller's decision, exactly as for the original
9
+ write; ``undo_apply`` records it either way.
10
10
 
11
11
  Note: recorded ``undo_params`` are the redacted safe-params captured at record
12
12
  time, so an inverse that would need a secret value cannot be replayed here — by
@@ -22,8 +22,22 @@ from queue_aiops.governance.undo import get_undo_store
22
22
 
23
23
 
24
24
  def _resolve_tool(name: str) -> Any:
25
- """Return the governed callable registered under ``name`` (or None)."""
25
+ """Return the governed callable registered under ``name`` (or None).
26
+
27
+ The CLI reaches this through ``undo apply``, whose process only imports
28
+ ``mcp_server.tools.undo`` — every other tool module is imported lazily inside
29
+ its own CLI command and so is absent here. Without forcing a full load,
30
+ ``undo_apply`` would fail with "inverse tool not registered" for every write
31
+ tool when driven from the CLI (the MCP entry point loads all modules, so it
32
+ never hit this). If the first lookup misses, import ``mcp_server.server`` —
33
+ which registers every ``@mcp.tool()`` as an import side effect — and retry.
34
+ Imported lazily to avoid the undo↔server cycle.
35
+ """
26
36
  tool = mcp._tool_manager._tools.get(name)
37
+ if tool is None:
38
+ import mcp_server.server # noqa: F401 — registers all tools as a side effect
39
+
40
+ tool = mcp._tool_manager._tools.get(name)
27
41
  return getattr(tool, "fn", None) if tool else None
28
42
 
29
43
 
@@ -79,9 +93,9 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
79
93
  def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None) -> dict:
80
94
  """[WRITE][risk=medium] Apply a recorded undo by dispatching its inverse tool.
81
95
 
82
- The inverse runs through its own governed tool, so its real risk tier and
83
- any approver requirement are enforced there. Pass dry_run=True to preview
84
- the inverse call without executing it. A token can only be applied once.
96
+ The inverse runs through its own governed tool, so it is audited under its
97
+ own risk tier. Pass dry_run=True to preview the inverse call without
98
+ executing it. A token can only be applied once.
85
99
 
86
100
  Args:
87
101
  undo_id: The undoId from undo_list (or an ``_undo_id`` in a write result).
@@ -98,12 +112,27 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
98
112
  )
99
113
 
100
114
  inverse_tool = rec["undo_tool"]
115
+ # Unreadable parameters must stop the replay, not be replaced with {}.
116
+ # Defaulting dispatched the inverse tool with NO ARGUMENTS: for a tool whose
117
+ # parameters all have defaults that runs a real, unintended operation, and
118
+ # dry_run previewed it as a legitimate no-arg inverse. Refusing is the only
119
+ # honest answer — the recorded intent is gone, so there is nothing to apply.
120
+ raw = rec["undo_params"]
101
121
  try:
102
- params = json.loads(rec["undo_params"]) if rec["undo_params"] else {}
103
- except (ValueError, TypeError):
104
- params = {}
122
+ params = json.loads(raw) if raw else {}
123
+ except (ValueError, TypeError) as exc:
124
+ raise ValueError(
125
+ f"Undo '{undo_id}' has unreadable recorded parameters, so its inverse "
126
+ f"cannot be replayed. Inspect the row in undo.db and re-run the "
127
+ f"inverse tool '{inverse_tool}' by hand if the change still needs "
128
+ f"reversing."
129
+ ) from exc
105
130
  if not isinstance(params, dict):
106
- params = {}
131
+ raise ValueError(
132
+ f"Undo '{undo_id}' recorded parameters of type "
133
+ f"'{type(params).__name__}' rather than an object, so its inverse "
134
+ f"cannot be replayed. Re-run the inverse tool '{inverse_tool}' by hand."
135
+ )
107
136
 
108
137
  fn = _resolve_tool(inverse_tool)
109
138
  if fn is None:
@@ -1,6 +1,6 @@
1
1
  """Governed broker-write MCP tools (the only state-changing tools).
2
2
 
3
- Every tool is wrapped with the governance harness (audit + graduated approval
3
+ Every tool is wrapped with the governance harness (audit + descriptive risk
4
4
  tier) and takes a ``dry_run`` preview. Reversible writes pass an ``undo=``
5
5
  callback that turns the fetched before-state into an inverse descriptor the
6
6
  harness records; irreversible ones (kill_client, purge_queue) record only the
@@ -214,9 +214,7 @@ def purge_queue(
214
214
 
215
215
  Reads the queue first so priorState carries the message count about to be
216
216
  destroyed (audit evidence); unacked messages are not purged. No undo —
217
- purged messages cannot be restored. Requires an approver
218
- (QUEUE_AUDIT_APPROVED_BY) under the graduated-autonomy policy. Pass
219
- dry_run=True to preview.
217
+ purged messages cannot be restored. Pass dry_run=True to preview.
220
218
 
221
219
  Args:
222
220
  vhost: The queue's vhost (the default vhost is '/').
@@ -243,9 +241,8 @@ def delete_queue(
243
241
 
244
242
  Reads the queue first so priorState carries the definition (durable /
245
243
  auto_delete / arguments) and message count; the recorded undo re-declares
246
- the queue with those properties — its messages are NOT restored. Requires
247
- an approver (QUEUE_AUDIT_APPROVED_BY) under the graduated-autonomy policy.
248
- Pass dry_run=True to preview.
244
+ the queue with those properties — its messages are NOT restored. Pass
245
+ dry_run=True to preview.
249
246
 
250
247
  Args:
251
248
  vhost: The queue's vhost (the default vhost is '/').
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "queue-aiops"
7
- version = "0.3.0"
7
+ version = "0.5.0"
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"
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "rich>=13.0,<16.0",
18
18
  "pyyaml>=6.0,<7.0",
19
19
  "cryptography>=42.0",
20
- "mcp[cli]>=1.10,<2.0",
20
+ "mcp[cli]>=2.0,<3.0",
21
21
  ]
22
22
 
23
23
  [dependency-groups]
@@ -1,7 +1,7 @@
1
1
  """queue-aiops — governed redis + rabbitmq broker operations for AI agents.
2
2
 
3
3
  Standalone and self-contained: the governance harness (audit, token budget,
4
- undo-token recording, graduated risk tiers, output sanitize) is
4
+ undo-token recording, descriptive risk tiers, output sanitize) is
5
5
  bundled under ``queue_aiops.governance`` — this package has no external
6
6
  skill-family dependency. Preview: not yet full-coverage.
7
7
  """
@@ -23,10 +23,18 @@ DryRunOption = Annotated[
23
23
 
24
24
 
25
25
  def _cli_error_types() -> tuple[type[BaseException], ...]:
26
- """Exceptions translated to a one-line teaching error instead of a traceback."""
26
+ """Exceptions translated to a one-line teaching error instead of a traceback.
27
+
28
+ ``BudgetExceeded`` (and the retained ``PolicyDenied`` type) are raised by
29
+ ``@governed_tool`` OUTSIDE the tool body, so ``tool_errors`` never sees them
30
+ and they never arrive as an ``{"error": ...}`` dict. Their message is the
31
+ teaching text (e.g. which budget was hit) — without them here such a refusal
32
+ reaches the CLI as a traceback instead.
33
+ """
27
34
  from queue_aiops.connection import QueueApiError
35
+ from queue_aiops.governance import BudgetExceeded, PolicyDenied
28
36
 
29
- return (QueueApiError, KeyError, OSError, ValueError)
37
+ return (QueueApiError, PolicyDenied, BudgetExceeded, KeyError, OSError, ValueError)
30
38
 
31
39
 
32
40
  def cli_errors(fn: Callable) -> Callable: