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.
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/PKG-INFO +61 -12
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/README.md +60 -11
- queue_aiops-0.2.1/RELEASE_NOTES.md +26 -0
- queue_aiops-0.2.1/docs/VERIFICATION.md +34 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/_shared.py +1 -1
- queue_aiops-0.2.1/mcp_server/server.py +76 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/rabbit_reads.py +16 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/redis_reads.py +12 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/undo.py +13 -2
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/pyproject.toml +1 -1
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/rabbit_cmds.py +28 -4
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/redis_cmds.py +14 -2
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/__init__.py +5 -1
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/decorators.py +12 -0
- queue_aiops-0.2.1/queue_aiops/governance/readonly.py +49 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/sanitize.py +25 -0
- queue_aiops-0.2.1/queue_aiops/ops/_util.py +94 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/analysis.py +13 -10
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/rabbit_reads.py +54 -33
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/redis_reads.py +63 -35
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/writes.py +8 -8
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/server.json +3 -3
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/SKILL.md +106 -37
- queue_aiops-0.2.1/skills/queue-aiops/references/agent-guardrails.md +109 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/capabilities.md +8 -1
- queue_aiops-0.2.1/tests/test_cli_rabbit_writes.py +164 -0
- queue_aiops-0.2.1/tests/test_cli_reads.py +249 -0
- queue_aiops-0.2.1/tests/test_cli_secret.py +111 -0
- queue_aiops-0.2.1/tests/test_connection.py +241 -0
- queue_aiops-0.2.1/tests/test_gov_audit.py +292 -0
- queue_aiops-0.2.1/tests/test_gov_decorators.py +452 -0
- queue_aiops-0.2.1/tests/test_gov_patterns.py +417 -0
- queue_aiops-0.2.1/tests/test_gov_policy.py +415 -0
- queue_aiops-0.2.1/tests/test_mcp_reads.py +211 -0
- queue_aiops-0.2.1/tests/test_optional_fields.py +184 -0
- queue_aiops-0.2.1/tests/test_readonly.py +142 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_reads.py +33 -2
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/uv.lock +1 -1
- queue_aiops-0.1.1/RELEASE_NOTES.md +0 -33
- queue_aiops-0.1.1/mcp_server/server.py +0 -38
- queue_aiops-0.1.1/queue_aiops/ops/_util.py +0 -55
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/.gitignore +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/CHANGELOG.md +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/LICENSE +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/SECURITY.md +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/analysis.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/overview.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/mcp_server/tools/writes.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/__init__.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/_common.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/_root.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/init.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/cli/undo.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/config.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/connection.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/budget.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/policy.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/governance/undo.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/ops/overview.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/cli-reference.md +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/skills/queue-aiops/references/setup-guide.md +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/smithery.yaml +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/conftest.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/fakes.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_analysis.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_cli_writes.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_doctor.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_governance_persistence.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_init.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_platform.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_secretstore.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_smoke.py +0 -0
- {queue_aiops-0.1.1 → queue_aiops-0.2.1}/tests/test_undo_executor.py +0 -0
- {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.
|
|
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
|
|
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
|
-
> **
|
|
37
|
-
> redis and rabbitmq are free/self-hostable (one lab container
|
|
38
|
-
> check is easy — `queue-aiops doctor` is the fastest
|
|
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
|
-
**
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
> **
|
|
20
|
-
> redis and rabbitmq are free/self-hostable (one lab container
|
|
21
|
-
> check is easy — `queue-aiops doctor` is the fastest
|
|
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
|
-
**
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|