queue-aiops 0.9.1__tar.gz → 0.9.3__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.9.1 → queue_aiops-0.9.3}/.claude-plugin/plugin.json +1 -1
  2. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/.mcp.json +1 -1
  3. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/CHANGELOG.md +20 -0
  4. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/PKG-INFO +2 -2
  5. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/README.md +1 -1
  6. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/openclaw.plugin.json +1 -1
  7. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/package.json +2 -2
  8. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/pyproject.toml +1 -1
  9. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/_common.py +40 -0
  10. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/analyze.py +5 -1
  11. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/overview.py +2 -1
  12. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/rabbit_cmds.py +8 -0
  13. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/redis_cmds.py +8 -0
  14. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/server.json +2 -2
  15. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/skills/queue-aiops/SKILL.md +1 -1
  16. queue_aiops-0.9.3/tests/test_cli_audited.py +131 -0
  17. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/uv.lock +1 -1
  18. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/.github/workflows/mcp-publish.yml +0 -0
  19. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/.github/workflows/publish.yml +0 -0
  20. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/.gitignore +0 -0
  21. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/Dockerfile +0 -0
  22. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/LICENSE +0 -0
  23. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/RELEASE_NOTES.md +0 -0
  24. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/SECURITY.md +0 -0
  25. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/docs/VERIFICATION.md +0 -0
  26. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/glama.json +0 -0
  27. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/__init__.py +0 -0
  28. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/_shared.py +0 -0
  29. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/server.py +0 -0
  30. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/__init__.py +0 -0
  31. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/analysis.py +0 -0
  32. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/overview.py +0 -0
  33. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/rabbit_reads.py +0 -0
  34. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/redis_reads.py +0 -0
  35. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/undo.py +0 -0
  36. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/mcp_server/tools/writes.py +0 -0
  37. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/__init__.py +0 -0
  38. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/__init__.py +0 -0
  39. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/_root.py +0 -0
  40. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/doctor.py +0 -0
  41. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/init.py +0 -0
  42. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/secret.py +0 -0
  43. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/cli/undo.py +0 -0
  44. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/config.py +0 -0
  45. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/connection.py +0 -0
  46. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/doctor.py +0 -0
  47. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/__init__.py +0 -0
  48. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/audit.py +0 -0
  49. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/budget.py +0 -0
  50. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/decorators.py +0 -0
  51. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/outcome.py +0 -0
  52. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/paths.py +0 -0
  53. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/patterns.py +0 -0
  54. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/policy.py +0 -0
  55. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/sanitize.py +0 -0
  56. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/governance/undo.py +0 -0
  57. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/__init__.py +0 -0
  58. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/_util.py +0 -0
  59. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/analysis.py +0 -0
  60. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/overview.py +0 -0
  61. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/rabbit_reads.py +0 -0
  62. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/redis_reads.py +0 -0
  63. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/ops/writes.py +0 -0
  64. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/platform.py +0 -0
  65. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/queue_aiops/secretstore.py +0 -0
  66. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/skills/queue-aiops/references/agent-guardrails.md +0 -0
  67. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/skills/queue-aiops/references/capabilities.md +0 -0
  68. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/skills/queue-aiops/references/cli-reference.md +0 -0
  69. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/skills/queue-aiops/references/setup-guide.md +0 -0
  70. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/smithery.yaml +0 -0
  71. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/conftest.py +0 -0
  72. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/fakes.py +0 -0
  73. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_analysis.py +0 -0
  74. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_cli_governed_exit.py +0 -0
  75. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_cli_rabbit_writes.py +0 -0
  76. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_cli_reads.py +0 -0
  77. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_cli_secret.py +0 -0
  78. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_cli_writes.py +0 -0
  79. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_connection.py +0 -0
  80. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_doctor.py +0 -0
  81. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_gov_audit.py +0 -0
  82. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_gov_decorators.py +0 -0
  83. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_gov_patterns.py +0 -0
  84. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_gov_policy.py +0 -0
  85. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_governance_persistence.py +0 -0
  86. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_init.py +0 -0
  87. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_int_fidelity.py +0 -0
  88. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_mcp_reads.py +0 -0
  89. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_optional_fields.py +0 -0
  90. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_platform.py +0 -0
  91. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_reads.py +0 -0
  92. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_secretstore.py +0 -0
  93. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_self_kill.py +0 -0
  94. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_self_lockout.py +0 -0
  95. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_smoke.py +0 -0
  96. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_undo_executor.py +0 -0
  97. {queue_aiops-0.9.1 → queue_aiops-0.9.3}/tests/test_writes.py +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "queue-aiops",
4
4
  "displayName": "Queue AIops",
5
- "version": "0.9.1",
5
+ "version": "0.9.3",
6
6
  "description": "Governed redis + rabbitmq ops: memory/latency/backlog/churn RCA, policies. 28 tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "queue-aiops==0.9.1",
8
+ "queue-aiops==0.9.3",
9
9
  "queue-aiops-mcp"
10
10
  ]
11
11
  }
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.9.3 — 2026-09-15
4
+
5
+ ### Fixed
6
+ - CLI commands that read the engine without calling an MCP tool now write an
7
+ audit row, as every MCP call does: they run through the same `@governed_tool`
8
+ harness, budget and runaway guard included. Before, an operator's CLI reads
9
+ left no trace in `audit.db`, contrary to the documented guarantee that MCP
10
+ and CLI are audited alike. A test fails if any engine command escapes it.
11
+
12
+ ## v0.9.2 — 2026-09-12
13
+
14
+ ### Changed
15
+ - **The ClawHub bundle plugin moved from `@aiops-tools/queue-aiops` to
16
+ `@zw008/queue-aiops`**, matching the publisher the skill has always been under.
17
+ ClawHub cannot move a package between scopes — the scope is the publisher
18
+ identity — so this is a republish under the new name; the old name is
19
+ withdrawn. Install with:
20
+ `openclaw plugins install clawhub:@zw008/queue-aiops`. Nothing about the Python
21
+ package, the CLI, the MCP server or the Claude Code plugin changes.
22
+
3
23
  ## v0.9.1 — 2026-09-12
4
24
 
5
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: queue-aiops
3
- Version: 0.9.1
3
+ Version: 0.9.3
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
@@ -59,7 +59,7 @@ The same bundle is published on [ClawHub](https://clawhub.ai/plugins), where one
59
59
  install delivers the skill and its MCP server together:
60
60
 
61
61
  ```bash
62
- openclaw plugins install clawhub:@aiops-tools/queue-aiops
62
+ openclaw plugins install clawhub:@zw008/queue-aiops
63
63
  openclaw skills info queue-aiops # expect: Visible to model: yes
64
64
  ```
65
65
 
@@ -42,7 +42,7 @@ The same bundle is published on [ClawHub](https://clawhub.ai/plugins), where one
42
42
  install delivers the skill and its MCP server together:
43
43
 
44
44
  ```bash
45
- openclaw plugins install clawhub:@aiops-tools/queue-aiops
45
+ openclaw plugins install clawhub:@zw008/queue-aiops
46
46
  openclaw skills info queue-aiops # expect: Visible to model: yes
47
47
  ```
48
48
 
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "queue-aiops==0.9.1",
14
+ "queue-aiops==0.9.3",
15
15
  "queue-aiops-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "@aiops-tools/queue-aiops",
3
- "version": "0.9.4",
2
+ "name": "@zw008/queue-aiops",
3
+ "version": "0.9.3",
4
4
  "description": "Governed redis + rabbitmq ops: memory/latency/backlog/churn RCA, policies. 28 tools.",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "queue-aiops"
7
- version = "0.9.1"
7
+ version = "0.9.3"
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"
@@ -153,3 +153,43 @@ def double_confirm(action: str, resource: str) -> None:
153
153
  f"Confirm 2/2: really {action} '{resource}'? This may be irreversible.",
154
154
  abort=True,
155
155
  )
156
+
157
+
158
+ def audited(fn: Callable) -> Callable:
159
+ """Audit a CLI command that reaches the engine without calling an MCP tool.
160
+
161
+ MCP tools write their audit row through ``@governed_tool``; a CLI command that
162
+ called the ops layer directly used to write none, so an operator's CLI reads
163
+ left no trail. This runs the command through the same harness: one audit row per
164
+ invocation (tool = the command function, params = its options), plus the budget
165
+ and runaway guard. ``typer.Exit`` is how a command ends: exit code 0 is audited
166
+ ``ok``, a non-zero code ``error``, and the exit is re-raised unchanged. Any other
167
+ exception is audited ``error`` and propagates to ``cli_errors`` as before.
168
+ """
169
+ import functools as _functools
170
+
171
+ from queue_aiops.governance import governed_tool
172
+
173
+ @_functools.wraps(fn)
174
+ def body(*args: Any, **kwargs: Any) -> Any:
175
+ try:
176
+ fn(*args, **kwargs)
177
+ except typer.Exit as exc:
178
+ code = exc.exit_code
179
+ outcome: dict[str, Any] = {"_exitCode": code}
180
+ if code:
181
+ outcome["error"] = f"command exited with code {code}"
182
+ return outcome
183
+ return None
184
+
185
+ governed = governed_tool(risk_level="low")(body)
186
+
187
+ @_functools.wraps(fn)
188
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
189
+ result = governed(*args, **kwargs)
190
+ if isinstance(result, dict) and "_exitCode" in result:
191
+ raise typer.Exit(result["_exitCode"])
192
+ return None
193
+
194
+ wrapper._is_audited_cli = True # type: ignore[attr-defined]
195
+ return wrapper
@@ -7,7 +7,7 @@ from typing import Annotated
7
7
 
8
8
  import typer
9
9
 
10
- from queue_aiops.cli._common import TargetOption, cli_errors, console, get_connection
10
+ from queue_aiops.cli._common import TargetOption, audited, cli_errors, console, get_connection
11
11
 
12
12
  analyze_app = typer.Typer(
13
13
  name="analyze",
@@ -19,6 +19,7 @@ analyze_app = typer.Typer(
19
19
 
20
20
  @analyze_app.command("memory")
21
21
  @cli_errors
22
+ @audited
22
23
  def analyze_memory(
23
24
  used_pct: Annotated[
24
25
  float, typer.Option("--used-pct", help="used/maxmemory %% pressure threshold")
@@ -41,6 +42,7 @@ def analyze_memory(
41
42
 
42
43
  @analyze_app.command("latency")
43
44
  @cli_errors
45
+ @audited
44
46
  def analyze_latency(
45
47
  slow_us: Annotated[
46
48
  float, typer.Option("--slow-us", help="Slowlog threshold in microseconds")
@@ -57,6 +59,7 @@ def analyze_latency(
57
59
 
58
60
  @analyze_app.command("backlog")
59
61
  @cli_errors
62
+ @audited
60
63
  def analyze_backlog(
61
64
  vhost: Annotated[
62
65
  str | None, typer.Option("--vhost", help="Restrict to one vhost")
@@ -76,6 +79,7 @@ def analyze_backlog(
76
79
 
77
80
  @analyze_app.command("churn")
78
81
  @cli_errors
82
+ @audited
79
83
  def analyze_churn(target: TargetOption = None) -> None:
80
84
  """Connection-churn analysis (both platforms): counts, rates, by source."""
81
85
  from queue_aiops.ops import analysis as ops
@@ -4,10 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
 
7
- from queue_aiops.cli._common import TargetOption, cli_errors, console, get_connection
7
+ from queue_aiops.cli._common import TargetOption, audited, cli_errors, console, get_connection
8
8
 
9
9
 
10
10
  @cli_errors
11
+ @audited
11
12
  def overview_cmd(target: TargetOption = None) -> None:
12
13
  """One-shot summary: platform + version + backlog/memory/client health."""
13
14
  from queue_aiops.ops import overview as ops
@@ -10,6 +10,7 @@ import typer
10
10
  from queue_aiops.cli._common import (
11
11
  DryRunOption,
12
12
  TargetOption,
13
+ audited,
13
14
  checked,
14
15
  cli_errors,
15
16
  console,
@@ -32,6 +33,7 @@ VhostOption = Annotated[
32
33
 
33
34
  @rabbitmq_app.command("overview")
34
35
  @cli_errors
36
+ @audited
35
37
  def rabbit_overview(target: TargetOption = None) -> None:
36
38
  """Broker identity + totals (queues, messages, connections, rates)."""
37
39
  from queue_aiops.ops import rabbit_reads as ops
@@ -42,6 +44,7 @@ def rabbit_overview(target: TargetOption = None) -> None:
42
44
 
43
45
  @rabbitmq_app.command("queues")
44
46
  @cli_errors
47
+ @audited
45
48
  def rabbit_queues(
46
49
  vhost: Annotated[
47
50
  str | None, typer.Option("--vhost", help="Restrict to one vhost")
@@ -63,6 +66,7 @@ def rabbit_queues(
63
66
 
64
67
  @rabbitmq_app.command("queue")
65
68
  @cli_errors
69
+ @audited
66
70
  def rabbit_queue(
67
71
  name: Annotated[str, typer.Argument(help="Queue name (from 'queues')")],
68
72
  vhost: VhostOption = "/",
@@ -77,6 +81,7 @@ def rabbit_queue(
77
81
 
78
82
  @rabbitmq_app.command("connections")
79
83
  @cli_errors
84
+ @audited
80
85
  def rabbit_connections(target: TargetOption = None) -> None:
81
86
  """Client connections grouped by peer host."""
82
87
  from queue_aiops.ops import rabbit_reads as ops
@@ -93,6 +98,7 @@ def rabbit_connections(target: TargetOption = None) -> None:
93
98
 
94
99
  @rabbitmq_app.command("channels")
95
100
  @cli_errors
101
+ @audited
96
102
  def rabbit_channels(target: TargetOption = None) -> None:
97
103
  """Channels with unacked/prefetch pressure."""
98
104
  from queue_aiops.ops import rabbit_reads as ops
@@ -109,6 +115,7 @@ def rabbit_channels(target: TargetOption = None) -> None:
109
115
 
110
116
  @rabbitmq_app.command("policies")
111
117
  @cli_errors
118
+ @audited
112
119
  def rabbit_policies(
113
120
  vhost: Annotated[
114
121
  str | None, typer.Option("--vhost", help="Restrict to one vhost")
@@ -130,6 +137,7 @@ def rabbit_policies(
130
137
 
131
138
  @rabbitmq_app.command("nodes")
132
139
  @cli_errors
140
+ @audited
133
141
  def rabbit_nodes(target: TargetOption = None) -> None:
134
142
  """Node memory/disk/fd posture + watermark alarms."""
135
143
  from queue_aiops.ops import rabbit_reads as ops
@@ -10,6 +10,7 @@ import typer
10
10
  from queue_aiops.cli._common import (
11
11
  DryRunOption,
12
12
  TargetOption,
13
+ audited,
13
14
  checked,
14
15
  cli_errors,
15
16
  console,
@@ -28,6 +29,7 @@ redis_app = typer.Typer(
28
29
 
29
30
  @redis_app.command("info")
30
31
  @cli_errors
32
+ @audited
31
33
  def redis_info(target: TargetOption = None) -> None:
32
34
  """Server identity + health basics (version, role, clients, ops/sec)."""
33
35
  from queue_aiops.ops import redis_reads as ops
@@ -38,6 +40,7 @@ def redis_info(target: TargetOption = None) -> None:
38
40
 
39
41
  @redis_app.command("memory")
40
42
  @cli_errors
43
+ @audited
41
44
  def redis_memory(target: TargetOption = None) -> None:
42
45
  """Memory posture: used vs maxmemory, policy, fragmentation."""
43
46
  from queue_aiops.ops import redis_reads as ops
@@ -48,6 +51,7 @@ def redis_memory(target: TargetOption = None) -> None:
48
51
 
49
52
  @redis_app.command("clients")
50
53
  @cli_errors
54
+ @audited
51
55
  def redis_clients(target: TargetOption = None) -> None:
52
56
  """Connected clients grouped by source address."""
53
57
  from queue_aiops.ops import redis_reads as ops
@@ -64,6 +68,7 @@ def redis_clients(target: TargetOption = None) -> None:
64
68
 
65
69
  @redis_app.command("slowlog")
66
70
  @cli_errors
71
+ @audited
67
72
  def redis_slowlog(
68
73
  count: Annotated[int, typer.Option("--count", "-n", help="Max entries")] = 128,
69
74
  target: TargetOption = None,
@@ -83,6 +88,7 @@ def redis_slowlog(
83
88
 
84
89
  @redis_app.command("config-get")
85
90
  @cli_errors
91
+ @audited
86
92
  def redis_config_get(
87
93
  pattern: Annotated[str, typer.Argument(help="Config-name glob, e.g. 'maxmemory*'")] = "*",
88
94
  target: TargetOption = None,
@@ -96,6 +102,7 @@ def redis_config_get(
96
102
 
97
103
  @redis_app.command("keyspace")
98
104
  @cli_errors
105
+ @audited
99
106
  def redis_keyspace(target: TargetOption = None) -> None:
100
107
  """Per-db key counts and expiry coverage."""
101
108
  from queue_aiops.ops import redis_reads as ops
@@ -106,6 +113,7 @@ def redis_keyspace(target: TargetOption = None) -> None:
106
113
 
107
114
  @redis_app.command("bigkeys")
108
115
  @cli_errors
116
+ @audited
109
117
  def redis_bigkeys(
110
118
  top: Annotated[int, typer.Option("--top", help="Rows to return, largest first")] = 20,
111
119
  target: TargetOption = None,
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/AIops-tools/Queue-AIops",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.9.1",
10
+ "version": "0.9.3",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "queue-aiops",
15
- "version": "0.9.1",
15
+ "version": "0.9.3",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -78,7 +78,7 @@ queue-aiops doctor
78
78
  Or as an OpenClaw plugin, which installs this skill and its MCP server together:
79
79
 
80
80
  ```bash
81
- openclaw plugins install clawhub:@aiops-tools/queue-aiops
81
+ openclaw plugins install clawhub:@zw008/queue-aiops
82
82
  openclaw skills info queue-aiops # expect: Visible to model: yes
83
83
  ```
84
84
 
@@ -0,0 +1,131 @@
1
+ """Every CLI command that reaches the engine is audited.
2
+
3
+ MCP tools write their audit row through ``@governed_tool``. CLI commands that called
4
+ the ops layer directly wrote none: on 2026-09-15 a read command of every tool in the
5
+ line, run in a scratch home, left no audit.db while the harness itself worked. Each
6
+ such command is now wrapped in ``audited`` (``cli/_common.py``); commands that call an
7
+ MCP tool are audited by that tool. This pins the helper's behaviour and that no
8
+ engine command escapes it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ import inspect
15
+ import sqlite3
16
+ import textwrap
17
+
18
+ import pytest
19
+ import typer
20
+ from typer.main import get_command
21
+
22
+ from queue_aiops.cli import app
23
+ from queue_aiops.cli._common import audited
24
+
25
+ pytestmark = pytest.mark.unit
26
+
27
+ #: Local-only commands: they never reach the engine (``undo`` goes through MCP tools).
28
+ EXEMPT = {"init", "doctor", "secret", "mcp", "undo"}
29
+
30
+
31
+ def _leaves() -> list[tuple[list[str], object]]:
32
+ found: list[tuple[list[str], object]] = []
33
+
34
+ def walk(cmd, path):
35
+ if hasattr(cmd, "list_commands"):
36
+ for name, sub in cmd.commands.items():
37
+ walk(sub, path + [name])
38
+ else:
39
+ found.append((path, cmd))
40
+
41
+ walk(get_command(app), [])
42
+ return found
43
+
44
+
45
+ def _chain(fn):
46
+ seen = []
47
+ while fn is not None and fn not in seen:
48
+ seen.append(fn)
49
+ fn = getattr(fn, "__wrapped__", None)
50
+ return seen
51
+
52
+
53
+ def _calls_an_mcp_tool(fn) -> bool:
54
+ source = inspect.getsource(fn)
55
+ if "mcp_server" in source:
56
+ return True
57
+ names = set()
58
+ for node in ast.parse(open(inspect.getsourcefile(fn)).read()).body:
59
+ if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("mcp_server"):
60
+ names |= {a.asname or a.name for a in node.names}
61
+ used = {n.id for n in ast.walk(ast.parse(textwrap.dedent(source))) if isinstance(n, ast.Name)}
62
+ return bool(names & used)
63
+
64
+
65
+ def test_every_engine_command_is_audited():
66
+ commands = [(path, cmd) for path, cmd in _leaves() if path and path[0] not in EXEMPT]
67
+ assert commands, "no commands found: the walker is broken, not the CLI clean"
68
+ missing = [
69
+ " ".join(path) for path, cmd in commands
70
+ if not any(getattr(f, "_is_audited_cli", False) for f in _chain(cmd.callback))
71
+ and not _calls_an_mcp_tool(inspect.unwrap(cmd.callback))
72
+ ]
73
+ assert not missing, f"CLI commands that leave no audit row: {missing}"
74
+
75
+
76
+ @pytest.fixture
77
+ def home(tmp_path, monkeypatch):
78
+ import queue_aiops.governance.audit as audit_mod
79
+ import queue_aiops.governance.budget as budget_mod
80
+
81
+ monkeypatch.setenv("QUEUE_AIOPS_HOME", str(tmp_path))
82
+ audit_mod.reset_engine()
83
+ budget_mod.reset_budget()
84
+ yield tmp_path
85
+ audit_mod.reset_engine()
86
+ budget_mod.reset_budget()
87
+
88
+
89
+ def _rows(home) -> list[tuple[str, str]]:
90
+ con = sqlite3.connect(home / "audit.db")
91
+ try:
92
+ return con.execute("SELECT tool, status FROM audit_log ORDER BY id").fetchall()
93
+ finally:
94
+ con.close()
95
+
96
+
97
+ def test_audited_records_success_exit_codes_and_exceptions(home):
98
+ @audited
99
+ def cmd_ok(target=None):
100
+ return None
101
+
102
+ @audited
103
+ def cmd_exit_zero(target=None):
104
+ raise typer.Exit()
105
+
106
+ @audited
107
+ def cmd_exit_one(target=None):
108
+ raise typer.Exit(1)
109
+
110
+ @audited
111
+ def cmd_raises(target=None):
112
+ raise RuntimeError("engine unreachable")
113
+
114
+ cmd_ok()
115
+ with pytest.raises(typer.Exit) as zero:
116
+ cmd_exit_zero()
117
+ assert zero.value.exit_code == 0
118
+ with pytest.raises(typer.Exit) as one:
119
+ cmd_exit_one()
120
+ assert one.value.exit_code == 1
121
+ with pytest.raises(RuntimeError, match="engine unreachable"):
122
+ cmd_raises()
123
+ assert _rows(home) == [("cmd_ok", "ok"), ("cmd_exit_zero", "ok"),
124
+ ("cmd_exit_one", "error"), ("cmd_raises", "error")]
125
+
126
+
127
+ def test_audited_keeps_the_command_signature_for_typer():
128
+ def cmd(limit: int = 5, target: str | None = None) -> None:
129
+ return None
130
+
131
+ assert list(inspect.signature(audited(cmd)).parameters) == ["limit", "target"]
@@ -812,7 +812,7 @@ wheels = [
812
812
 
813
813
  [[package]]
814
814
  name = "queue-aiops"
815
- version = "0.9.1"
815
+ version = "0.9.3"
816
816
  source = { editable = "." }
817
817
  dependencies = [
818
818
  { name = "cryptography" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes