queue-aiops 0.2.2__tar.gz → 0.4.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.
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/CHANGELOG.md +20 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/PKG-INFO +35 -48
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/README.md +33 -46
- queue_aiops-0.4.0/RELEASE_NOTES.md +29 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/SECURITY.md +6 -5
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/docs/VERIFICATION.md +2 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/_shared.py +39 -4
- queue_aiops-0.4.0/mcp_server/server.py +51 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/undo.py +43 -11
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/writes.py +21 -7
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/pyproject.toml +2 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/__init__.py +1 -1
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/_common.py +32 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/init.py +0 -38
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/rabbit_cmds.py +37 -14
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/redis_cmds.py +15 -5
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/undo.py +11 -5
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/config.py +14 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/connection.py +17 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/__init__.py +11 -5
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/budget.py +19 -22
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/decorators.py +97 -89
- queue_aiops-0.4.0/queue_aiops/governance/outcome.py +104 -0
- queue_aiops-0.4.0/queue_aiops/governance/policy.py +67 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/sanitize.py +11 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/undo.py +52 -13
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/redis_reads.py +16 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/writes.py +131 -1
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/server.json +2 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/skills/queue-aiops/SKILL.md +23 -15
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/skills/queue-aiops/references/agent-guardrails.md +29 -13
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/skills/queue-aiops/references/capabilities.md +5 -6
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/skills/queue-aiops/references/cli-reference.md +3 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/skills/queue-aiops/references/setup-guide.md +5 -6
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/conftest.py +3 -4
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/fakes.py +8 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_cli_rabbit_writes.py +43 -3
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_cli_writes.py +49 -7
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_gov_decorators.py +11 -28
- queue_aiops-0.4.0/tests/test_gov_policy.py +56 -0
- queue_aiops-0.4.0/tests/test_governance_persistence.py +354 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_init.py +5 -16
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_optional_fields.py +4 -1
- queue_aiops-0.4.0/tests/test_self_kill.py +165 -0
- queue_aiops-0.4.0/tests/test_self_lockout.py +222 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_smoke.py +30 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_undo_executor.py +22 -1
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_writes.py +9 -2
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/uv.lock +1 -1
- queue_aiops-0.2.2/RELEASE_NOTES.md +0 -29
- queue_aiops-0.2.2/mcp_server/server.py +0 -76
- queue_aiops-0.2.2/queue_aiops/governance/policy.py +0 -430
- queue_aiops-0.2.2/queue_aiops/governance/readonly.py +0 -49
- queue_aiops-0.2.2/tests/test_gov_policy.py +0 -415
- queue_aiops-0.2.2/tests/test_governance_persistence.py +0 -175
- queue_aiops-0.2.2/tests/test_readonly.py +0 -142
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/.gitignore +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/LICENSE +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/analysis.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/overview.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/rabbit_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/mcp_server/tools/redis_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/_root.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/_util.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/analysis.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/overview.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/ops/rabbit_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/smithery.yaml +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_analysis.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_cli_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_cli_secret.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_connection.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_doctor.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_gov_audit.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_gov_patterns.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_mcp_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_platform.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_reads.py +0 -0
- {queue_aiops-0.2.2 → queue_aiops-0.4.0}/tests/test_secretstore.py +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.0 — 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Changed (BREAKING)
|
|
6
|
+
- **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.
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
See RELEASE_NOTES.md for tool-specific changes.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## v0.3.0 — 2026-07-20
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`redis_config_set` refuses the parameters that lock this tool out of the server**: `requirepass`, `masterauth`, `bind`, `protected-mode`, `maxclients`, `port`, `unixsocket`, `aclfile`, and anything `tls-*`.
|
|
16
|
+
- `list_clients` no longer returns this tool's own connection, and `redis_kill_client` refuses it — by id or by address.
|
|
17
|
+
- Harness: a write whose response is lost is audited `status=unknown`, not `error` — it may have taken effect. Undo tokens gain `effectVerified` (undo.db migrated in place).
|
|
18
|
+
- Harness: a dry-run no longer records an undo token, and no longer requires a named approver. Guards now run on the preview path.
|
|
19
|
+
- Truncated strings end in an ellipsis instead of being cut silently; error messages are capped at 800 chars, not 300.
|
|
20
|
+
|
|
21
|
+
See RELEASE_NOTES.md for the full detail.
|
|
22
|
+
|
|
3
23
|
## v0.1.1 — 2026-07-17
|
|
4
24
|
|
|
5
25
|
### Fixed
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queue-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
-
Author-email:
|
|
5
|
+
Author-email: Wei Zhou <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
@@ -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
|
-
##
|
|
62
|
+
## What this tool does, and does not, decide
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
>
|
|
94
|
-
>
|
|
95
|
-
>
|
|
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
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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`
|
|
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
|
-
##
|
|
45
|
+
## What this tool does, and does not, decide
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
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
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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`
|
|
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
|
-
- **
|
|
44
|
-
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
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 —
|
|
@@ -17,17 +17,46 @@ from collections.abc import Callable
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any, Optional
|
|
19
19
|
|
|
20
|
+
import httpx
|
|
21
|
+
import redis
|
|
20
22
|
from mcp.server.fastmcp import FastMCP
|
|
21
23
|
|
|
22
24
|
from queue_aiops.config import load_config
|
|
23
25
|
from queue_aiops.connection import ConnectionManager, QueueApiError
|
|
24
|
-
from queue_aiops.governance import sanitize
|
|
26
|
+
from queue_aiops.governance import mark_unknown, sanitize
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
28
30
|
_DOCTOR_HINT = "Run 'queue-aiops doctor' to verify connectivity and credentials."
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
# Failures that leave the request's fate genuinely undetermined: the bytes
|
|
34
|
+
# went out and either the response or the rest of the connection was lost. A
|
|
35
|
+
# write that hits one of these MAY have taken effect on the server.
|
|
36
|
+
#
|
|
37
|
+
# Deliberately narrow. Connect errors and pool timeouts mean the request never
|
|
38
|
+
# left this process, and an API error carrying a status means the server
|
|
39
|
+
# answered — all are ordinary failures where nothing, or a known something,
|
|
40
|
+
# happened. Marking them 'unknown' would cry wolf on every unreachable host.
|
|
41
|
+
# redis TimeoutError means the command went out and the reply did not come
|
|
42
|
+
# back. redis ConnectionError is excluded: it also covers a connection that
|
|
43
|
+
# was never established.
|
|
44
|
+
_UNDETERMINED_ERRORS = (
|
|
45
|
+
httpx.ReadTimeout,
|
|
46
|
+
httpx.WriteTimeout,
|
|
47
|
+
httpx.ReadError,
|
|
48
|
+
httpx.WriteError,
|
|
49
|
+
httpx.RemoteProtocolError,
|
|
50
|
+
redis.exceptions.TimeoutError,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Long enough to carry the remediation sentence. These messages teach the
|
|
55
|
+
# caller what to do instead, and that clause comes last — a 300-char cap cut
|
|
56
|
+
# it off silently on every refusal long enough to need one.
|
|
57
|
+
_ERROR_MAX = 800
|
|
58
|
+
|
|
59
|
+
|
|
31
60
|
def _safe_error(exc: Exception, tool: str) -> str:
|
|
32
61
|
"""Return an agent-safe error string; log full detail server-side only."""
|
|
33
62
|
logger.error("Tool %s failed", tool, exc_info=True)
|
|
@@ -41,7 +70,7 @@ def _safe_error(exc: Exception, tool: str) -> str:
|
|
|
41
70
|
QueueApiError,
|
|
42
71
|
)
|
|
43
72
|
if isinstance(exc, _passthrough):
|
|
44
|
-
return sanitize(str(exc),
|
|
73
|
+
return sanitize(str(exc), _ERROR_MAX)
|
|
45
74
|
return f"{type(exc).__name__}: operation failed."
|
|
46
75
|
|
|
47
76
|
|
|
@@ -65,7 +94,13 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
65
94
|
return [{"error": msg, "hint": _DOCTOR_HINT}]
|
|
66
95
|
if shape == "str":
|
|
67
96
|
return f"Error: {msg} {_DOCTOR_HINT}"
|
|
68
|
-
|
|
97
|
+
payload = {"error": msg, "hint": _DOCTOR_HINT}
|
|
98
|
+
# Flatten the exception into a dict and its type is gone
|
|
99
|
+
# for good — so classify here, while it is still known,
|
|
100
|
+
# whether the operation may nonetheless have taken effect.
|
|
101
|
+
if isinstance(e, _UNDETERMINED_ERRORS):
|
|
102
|
+
return mark_unknown(payload)
|
|
103
|
+
return payload
|
|
69
104
|
|
|
70
105
|
return wrapper
|
|
71
106
|
|
|
@@ -85,7 +120,7 @@ mcp = FastMCP(
|
|
|
85
120
|
"heuristics that show their numbers. Governed writes — config_set and "
|
|
86
121
|
"set_policy/delete_policy (reversible, undo-recorded), declare_queue, "
|
|
87
122
|
"kill_client (priorState only), plus purge_queue / delete_queue at "
|
|
88
|
-
"risk=high with a dry_run preview
|
|
123
|
+
"risk=high with a dry_run preview (delete_queue's undo "
|
|
89
124
|
"re-declares the captured definition; messages are not restored). Every "
|
|
90
125
|
"tool runs through the queue-aiops governance harness (audit / budget / "
|
|
91
126
|
"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
|
|
7
|
-
the inverse itself is destructive —
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
@@ -41,6 +41,11 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
|
|
|
41
41
|
length coincidence: when it is true there are MORE tokens than shown, so
|
|
42
42
|
re-run with a higher ``limit`` rather than reporting the list as complete.
|
|
43
43
|
|
|
44
|
+
Each entry carries ``effectVerified``. False means the original write lost
|
|
45
|
+
its response, so the change it reverses is PROBABLE, not confirmed — check
|
|
46
|
+
the live state before applying, and do not report the result as a restore
|
|
47
|
+
of a state that may never have been reached.
|
|
48
|
+
|
|
44
49
|
Args:
|
|
45
50
|
limit: Max rows to return (default 50, capped at 500).
|
|
46
51
|
target: Unused (undo state is host-local); accepted for CLI uniformity.
|
|
@@ -61,6 +66,7 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
|
|
|
61
66
|
"originalTool": r["tool"],
|
|
62
67
|
"inverseTool": r["undo_tool"],
|
|
63
68
|
"note": r.get("note", ""),
|
|
69
|
+
"effectVerified": bool(r.get("effect_verified", 1)),
|
|
64
70
|
}
|
|
65
71
|
for r in rows
|
|
66
72
|
],
|
|
@@ -73,9 +79,9 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
|
|
|
73
79
|
def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None) -> dict:
|
|
74
80
|
"""[WRITE][risk=medium] Apply a recorded undo by dispatching its inverse tool.
|
|
75
81
|
|
|
76
|
-
The inverse runs through its own governed tool, so
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
The inverse runs through its own governed tool, so it is audited under its
|
|
83
|
+
own risk tier. Pass dry_run=True to preview the inverse call without
|
|
84
|
+
executing it. A token can only be applied once.
|
|
79
85
|
|
|
80
86
|
Args:
|
|
81
87
|
undo_id: The undoId from undo_list (or an ``_undo_id`` in a write result).
|
|
@@ -92,12 +98,27 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
|
|
|
92
98
|
)
|
|
93
99
|
|
|
94
100
|
inverse_tool = rec["undo_tool"]
|
|
101
|
+
# Unreadable parameters must stop the replay, not be replaced with {}.
|
|
102
|
+
# Defaulting dispatched the inverse tool with NO ARGUMENTS: for a tool whose
|
|
103
|
+
# parameters all have defaults that runs a real, unintended operation, and
|
|
104
|
+
# dry_run previewed it as a legitimate no-arg inverse. Refusing is the only
|
|
105
|
+
# honest answer — the recorded intent is gone, so there is nothing to apply.
|
|
106
|
+
raw = rec["undo_params"]
|
|
95
107
|
try:
|
|
96
|
-
params = json.loads(
|
|
97
|
-
except (ValueError, TypeError):
|
|
98
|
-
|
|
108
|
+
params = json.loads(raw) if raw else {}
|
|
109
|
+
except (ValueError, TypeError) as exc:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Undo '{undo_id}' has unreadable recorded parameters, so its inverse "
|
|
112
|
+
f"cannot be replayed. Inspect the row in undo.db and re-run the "
|
|
113
|
+
f"inverse tool '{inverse_tool}' by hand if the change still needs "
|
|
114
|
+
f"reversing."
|
|
115
|
+
) from exc
|
|
99
116
|
if not isinstance(params, dict):
|
|
100
|
-
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Undo '{undo_id}' recorded parameters of type "
|
|
119
|
+
f"'{type(params).__name__}' rather than an object, so its inverse "
|
|
120
|
+
f"cannot be replayed. Re-run the inverse tool '{inverse_tool}' by hand."
|
|
121
|
+
)
|
|
101
122
|
|
|
102
123
|
fn = _resolve_tool(inverse_tool)
|
|
103
124
|
if fn is None:
|
|
@@ -105,10 +126,12 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
|
|
|
105
126
|
f"Inverse tool '{inverse_tool}' is not registered on this server; cannot apply."
|
|
106
127
|
)
|
|
107
128
|
|
|
129
|
+
effect_verified = bool(rec.get("effect_verified", 1))
|
|
108
130
|
if dry_run:
|
|
109
131
|
return {
|
|
110
132
|
"dryRun": True,
|
|
111
133
|
"undoId": undo_id,
|
|
134
|
+
"effectVerified": effect_verified,
|
|
112
135
|
"wouldApply": {"tool": inverse_tool, "params": params},
|
|
113
136
|
}
|
|
114
137
|
|
|
@@ -127,6 +150,15 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
|
|
|
127
150
|
return {
|
|
128
151
|
"undoId": undo_id,
|
|
129
152
|
"applied": applied,
|
|
153
|
+
"effectVerified": effect_verified,
|
|
130
154
|
"inverseTool": inverse_tool,
|
|
131
155
|
"result": result,
|
|
156
|
+
"note": (
|
|
157
|
+
""
|
|
158
|
+
if effect_verified
|
|
159
|
+
else "The original write lost its response, so it was never confirmed "
|
|
160
|
+
"to have taken effect. This inverse ran regardless; report the "
|
|
161
|
+
"CURRENT server state, not a restore that may have had nothing "
|
|
162
|
+
"to restore."
|
|
163
|
+
),
|
|
132
164
|
}
|
|
@@ -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 +
|
|
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
|
|
@@ -141,6 +141,13 @@ def redis_config_set(
|
|
|
141
141
|
restores its prior value. Runtime-only — not persisted to the config file.
|
|
142
142
|
Pass dry_run=True to preview.
|
|
143
143
|
|
|
144
|
+
Refuses the parameters that would lock this tool out of the instance
|
|
145
|
+
(requirepass, masterauth, bind, port, protected-mode, maxclients,
|
|
146
|
+
unixsocket, aclfile, tls-*) — the undo could never be replayed over a
|
|
147
|
+
connection those settings break. Change those in redis.conf and restart.
|
|
148
|
+
The refusal applies under dry_run too: a preview whose real call would be
|
|
149
|
+
refused must report that, not a green 'wouldSet'.
|
|
150
|
+
|
|
144
151
|
Args:
|
|
145
152
|
parameter: Config parameter name (e.g. maxmemory-policy), from redis_config_get.
|
|
146
153
|
value: New value.
|
|
@@ -148,6 +155,9 @@ def redis_config_set(
|
|
|
148
155
|
target: redis target name from config; omit for the default.
|
|
149
156
|
"""
|
|
150
157
|
conn = _get_connection(target)
|
|
158
|
+
# Ahead of the dry_run return: a preview whose real call would be refused
|
|
159
|
+
# must say so, or the caller reads the refusal as transient and retries.
|
|
160
|
+
ops.guard_config_set(parameter)
|
|
151
161
|
if dry_run:
|
|
152
162
|
return {"dryRun": True, "wouldSet": {"parameter": parameter, "value": value}}
|
|
153
163
|
return ops.config_set(conn, parameter, value)
|
|
@@ -169,6 +179,10 @@ def redis_kill_client(
|
|
|
169
179
|
is recorded; most clients transparently reconnect. Pass dry_run=True to
|
|
170
180
|
preview.
|
|
171
181
|
|
|
182
|
+
Refuses this tool's own connection, by id or by addr — including under
|
|
183
|
+
dry_run, which must report a refusal rather than preview a call that will be
|
|
184
|
+
refused.
|
|
185
|
+
|
|
172
186
|
Args:
|
|
173
187
|
client_id: Client id from redis_clients (preferred).
|
|
174
188
|
addr: Or the client's 'ip:port' address.
|
|
@@ -176,6 +190,9 @@ def redis_kill_client(
|
|
|
176
190
|
target: redis target name from config; omit for the default.
|
|
177
191
|
"""
|
|
178
192
|
conn = _get_connection(target)
|
|
193
|
+
# Ahead of the dry_run return: a preview whose real call would be refused
|
|
194
|
+
# must say so, or the caller reads the refusal as transient and retries.
|
|
195
|
+
ops.guard_kill_client(conn, client_id=client_id, addr=addr)
|
|
179
196
|
if dry_run:
|
|
180
197
|
return {"dryRun": True, "wouldKill": {"clientId": client_id, "addr": addr}}
|
|
181
198
|
return ops.kill_client(conn, client_id=client_id, addr=addr)
|
|
@@ -197,9 +214,7 @@ def purge_queue(
|
|
|
197
214
|
|
|
198
215
|
Reads the queue first so priorState carries the message count about to be
|
|
199
216
|
destroyed (audit evidence); unacked messages are not purged. No undo —
|
|
200
|
-
purged messages cannot be restored.
|
|
201
|
-
(QUEUE_AUDIT_APPROVED_BY) under the graduated-autonomy policy. Pass
|
|
202
|
-
dry_run=True to preview.
|
|
217
|
+
purged messages cannot be restored. Pass dry_run=True to preview.
|
|
203
218
|
|
|
204
219
|
Args:
|
|
205
220
|
vhost: The queue's vhost (the default vhost is '/').
|
|
@@ -226,9 +241,8 @@ def delete_queue(
|
|
|
226
241
|
|
|
227
242
|
Reads the queue first so priorState carries the definition (durable /
|
|
228
243
|
auto_delete / arguments) and message count; the recorded undo re-declares
|
|
229
|
-
the queue with those properties — its messages are NOT restored.
|
|
230
|
-
|
|
231
|
-
Pass dry_run=True to preview.
|
|
244
|
+
the queue with those properties — its messages are NOT restored. Pass
|
|
245
|
+
dry_run=True to preview.
|
|
232
246
|
|
|
233
247
|
Args:
|
|
234
248
|
vhost: The queue's vhost (the default vhost is '/').
|