proxy-aiops 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/PKG-INFO +55 -11
  2. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/README.md +54 -10
  3. proxy_aiops-0.2.0/RELEASE_NOTES.md +48 -0
  4. proxy_aiops-0.2.0/docs/VERIFICATION.md +191 -0
  5. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/_shared.py +1 -1
  6. proxy_aiops-0.2.0/mcp_server/server.py +78 -0
  7. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/configread.py +4 -0
  8. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/traffic.py +10 -0
  9. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/undo.py +13 -2
  10. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/configcmd.py +7 -1
  11. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/__init__.py +5 -1
  12. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/decorators.py +12 -0
  13. proxy_aiops-0.2.0/proxy_aiops/governance/readonly.py +49 -0
  14. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/sanitize.py +25 -0
  15. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/_util.py +18 -1
  16. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/analysis.py +16 -16
  17. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/certs.py +2 -2
  18. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/configread.py +13 -4
  19. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/routes.py +16 -14
  20. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/services.py +18 -18
  21. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/status.py +11 -11
  22. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/traffic.py +13 -5
  23. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/pyproject.toml +1 -1
  24. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/server.json +3 -3
  25. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/SKILL.md +104 -39
  26. proxy_aiops-0.2.0/skills/proxy-aiops/references/agent-guardrails.md +106 -0
  27. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/capabilities.md +8 -1
  28. proxy_aiops-0.2.0/tests/test_cli_reads.py +240 -0
  29. proxy_aiops-0.2.0/tests/test_cli_secret.py +139 -0
  30. proxy_aiops-0.2.0/tests/test_connection.py +222 -0
  31. proxy_aiops-0.2.0/tests/test_gov_audit.py +292 -0
  32. proxy_aiops-0.2.0/tests/test_gov_decorators.py +452 -0
  33. proxy_aiops-0.2.0/tests/test_gov_patterns.py +417 -0
  34. proxy_aiops-0.2.0/tests/test_gov_policy.py +415 -0
  35. proxy_aiops-0.2.0/tests/test_mcp_read_tools.py +162 -0
  36. proxy_aiops-0.2.0/tests/test_ops_extra.py +238 -0
  37. proxy_aiops-0.2.0/tests/test_optional_fields.py +202 -0
  38. proxy_aiops-0.2.0/tests/test_readonly.py +142 -0
  39. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/uv.lock +1 -1
  40. proxy_aiops-0.1.0/RELEASE_NOTES.md +0 -61
  41. proxy_aiops-0.1.0/mcp_server/server.py +0 -40
  42. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.github/workflows/mcp-publish.yml +0 -0
  43. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.github/workflows/publish.yml +0 -0
  44. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.gitignore +0 -0
  45. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/CHANGELOG.md +0 -0
  46. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/LICENSE +0 -0
  47. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/SECURITY.md +0 -0
  48. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/__init__.py +0 -0
  49. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/__init__.py +0 -0
  50. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/analysis.py +0 -0
  51. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/certs.py +0 -0
  52. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/routes.py +0 -0
  53. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/services.py +0 -0
  54. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/status.py +0 -0
  55. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/writes.py +0 -0
  56. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/__init__.py +0 -0
  57. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/__init__.py +0 -0
  58. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/_common.py +0 -0
  59. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/_root.py +0 -0
  60. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/analyze.py +0 -0
  61. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/certs.py +0 -0
  62. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/doctor.py +0 -0
  63. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/init.py +0 -0
  64. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/overview.py +0 -0
  65. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/routes.py +0 -0
  66. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/secret.py +0 -0
  67. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/server.py +0 -0
  68. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/services.py +0 -0
  69. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/undo.py +0 -0
  70. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/config.py +0 -0
  71. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/connection.py +0 -0
  72. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/doctor.py +0 -0
  73. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/audit.py +0 -0
  74. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/budget.py +0 -0
  75. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/paths.py +0 -0
  76. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/patterns.py +0 -0
  77. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/policy.py +0 -0
  78. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/undo.py +0 -0
  79. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/__init__.py +0 -0
  80. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/overview.py +0 -0
  81. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/writes.py +0 -0
  82. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/platform.py +0 -0
  83. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/secretstore.py +0 -0
  84. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/cli-reference.md +0 -0
  85. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/setup-guide.md +0 -0
  86. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/smithery.yaml +0 -0
  87. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/conftest.py +0 -0
  88. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_analysis.py +0 -0
  89. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_cli_writes.py +0 -0
  90. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_doctor.py +0 -0
  91. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_governance_persistence.py +0 -0
  92. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_init.py +0 -0
  93. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_platform.py +0 -0
  94. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_reads.py +0 -0
  95. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_secretstore.py +0 -0
  96. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_smoke.py +0 -0
  97. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_undo_executor.py +0 -0
  98. {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_writes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxy-aiops
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Governed AI-ops for Traefik + Caddy + HAProxy reverse proxies / load balancers: routes, services, upstream health, TLS cert expiry sweep, 5xx error-rate RCA, route conflict analysis, and governed writes (caddy config, haproxy server state/weight) with a built-in governance harness (audit, budget, undo, risk tiers)
5
5
  Author-email: wei <zhouwei008@gmail.com>
6
6
  License-Expression: MIT
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  <!-- mcp-name: io.github.AIops-tools/proxy-aiops -->
18
18
 
19
- # Proxy AIops (preview)
19
+ # Proxy AIops
20
20
 
21
21
  Governed, audited AI-ops for **Traefik**, **Caddy** and **HAProxy** reverse proxies / load balancers — for AI agents (via MCP) and humans (via CLI).
22
22
 
@@ -57,7 +57,47 @@ risk-tier approval, undo-token recording, and output sanitisation.
57
57
  restores the prior value) — all with `dry_run` previews; delete/load are
58
58
  **risk=high** behind an approver gate.
59
59
 
60
- ## Tool inventory (26 tools)
60
+ ## Security: read-only mode
61
+
62
+ This tool is meant to be handed to an AI agent, so its safety story is enforced
63
+ by the server rather than requested in a prompt:
64
+
65
+ ```bash
66
+ export PROXY_READ_ONLY=1
67
+ ```
68
+
69
+ With that set, the **6 write tools are never registered**. An MCP client
70
+ lists **22 tools instead of 28** — the writes are not hidden, not
71
+ gated behind a flag, and not merely refused when called. They are absent from
72
+ the session. A model cannot invoke a tool it was never offered, and cannot be
73
+ argued into one.
74
+
75
+ That distinction is the whole point. A tool that exists but refuses still invites
76
+ retry loops and "I'll describe the call instead" behaviour from smaller models,
77
+ and it leaves a reviewer trusting a promise. An absent tool is a fact you can
78
+ check: connect, list the tools, and see that the writes are not there.
79
+
80
+ Enforcement is two layers deep, so the switch cannot be sidestepped by changing
81
+ entry point:
82
+
83
+ | Layer | What it does | Covers |
84
+ |---|---|---|
85
+ | `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
86
+ | MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
87
+
88
+ Read operations are unaffected, and every call is still audited to
89
+ `~/.proxy-aiops/audit.db`.
90
+
91
+ > The read/write split is derived from each tool's declared `risk_level`, and a
92
+ > test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
93
+ > tool's own documentation — so a write can't quietly present itself as a read.
94
+
95
+ Running a smaller / local model? See
96
+ [agent-guardrails.md](skills/proxy-aiops/references/agent-guardrails.md) — it lists
97
+ the guardrails this tool now enforces for you (so you don't spend prompt budget
98
+ restating them) and gives a ready-made system prompt for what's left.
99
+
100
+ ## Tool inventory (28 tools)
61
101
 
62
102
  | Domain | Tools | # | Kind |
63
103
  |--------|-------|:-:|------|
@@ -71,6 +111,7 @@ risk-tier approval, undo-token recording, and output sanitisation.
71
111
  | **Writes (caddy)** | `set_config_value` | 1 | write (**med**) |
72
112
  | **Writes (caddy)** | `delete_config_path`, `load_config` | 2 | write (**high**) |
73
113
  | **Writes (haproxy)** | `set_server_state`, `set_server_weight` | 2 | write (**med**) |
114
+ | **Undo** | `undo_list`, `undo_apply` | 2 | read / write |
74
115
 
75
116
  Reversible writes record an inverse **undo descriptor** built from the real fetched
76
117
  before-state (`set_config_value` restores the prior subtree; `delete_config_path`
@@ -191,17 +232,20 @@ Every MCP tool is wrapped by `@governed_tool`:
191
232
  - **Sanitisation** — all proxy-returned text is bounded + control-character
192
233
  sanitised before it reaches the agent.
193
234
 
194
- ## Preview status
235
+ ## Platform support & verification status
195
236
 
196
237
  - **Platforms**: Traefik (API + /metrics), Caddy (admin API), HAProxy (Data Plane
197
238
  API v2).
198
- - **Preview mock-validated only. Not run against a live proxy.** All behaviour is
199
- validated against mocked JSON/metrics responses; the concrete REST paths are
200
- modelled from each project's public API and need live verification. All three
201
- platforms are free and self-hostable (a small container-compose lab with
202
- traefik + caddy + haproxy/dataplaneapi is a one-evening setup), so
203
- `proxy-aiops doctor` a health/info probe per platform is the fastest
204
- live check.
239
+ - **Test coverage**: behaviour is validated against mocked JSON/metrics responses
240
+ every module imports, every MCP tool carries the governance marker, the four flagship
241
+ analyses are unit-tested against synthetic telemetry, the support matrix is asserted
242
+ to raise teaching errors (never a silent no-op), and reversible writes are asserted to
243
+ record the correct inverse undo descriptor. The concrete REST paths are modelled from
244
+ each project's public API and have not yet been exercised against a live proxy. See
245
+ [docs/VERIFICATION.md](docs/VERIFICATION.md) for the checklist a live run must
246
+ satisfy. All three platforms are free and self-hostable (a small container-compose lab
247
+ with traefik + caddy + haproxy/dataplaneapi is a one-evening setup), and
248
+ `proxy-aiops doctor` — a health/info probe per platform — is the fastest live check.
205
249
  - **Routing note**: this tool operates reverse proxies / load balancers. Do NOT
206
250
  use it for firewall rules — use firewall-aiops.
207
251
  - **Missing a capability?** Open an issue or PR at
@@ -1,6 +1,6 @@
1
1
  <!-- mcp-name: io.github.AIops-tools/proxy-aiops -->
2
2
 
3
- # Proxy AIops (preview)
3
+ # Proxy AIops
4
4
 
5
5
  Governed, audited AI-ops for **Traefik**, **Caddy** and **HAProxy** reverse proxies / load balancers — for AI agents (via MCP) and humans (via CLI).
6
6
 
@@ -41,7 +41,47 @@ risk-tier approval, undo-token recording, and output sanitisation.
41
41
  restores the prior value) — all with `dry_run` previews; delete/load are
42
42
  **risk=high** behind an approver gate.
43
43
 
44
- ## Tool inventory (26 tools)
44
+ ## Security: read-only mode
45
+
46
+ This tool is meant to be handed to an AI agent, so its safety story is enforced
47
+ by the server rather than requested in a prompt:
48
+
49
+ ```bash
50
+ export PROXY_READ_ONLY=1
51
+ ```
52
+
53
+ With that set, the **6 write tools are never registered**. An MCP client
54
+ lists **22 tools instead of 28** — the writes are not hidden, not
55
+ gated behind a flag, and not merely refused when called. They are absent from
56
+ the session. A model cannot invoke a tool it was never offered, and cannot be
57
+ argued into one.
58
+
59
+ That distinction is the whole point. A tool that exists but refuses still invites
60
+ retry loops and "I'll describe the call instead" behaviour from smaller models,
61
+ and it leaves a reviewer trusting a promise. An absent tool is a fact you can
62
+ check: connect, list the tools, and see that the writes are not there.
63
+
64
+ Enforcement is two layers deep, so the switch cannot be sidestepped by changing
65
+ entry point:
66
+
67
+ | Layer | What it does | Covers |
68
+ |---|---|---|
69
+ | `@governed_tool` harness | refuses every non-read operation outright | MCP, CLI, and in-process callers |
70
+ | MCP registration | write tools are removed from `list_tools()` | anything speaking MCP |
71
+
72
+ Read operations are unaffected, and every call is still audited to
73
+ `~/.proxy-aiops/audit.db`.
74
+
75
+ > The read/write split is derived from each tool's declared `risk_level`, and a
76
+ > test asserts that this never disagrees with the `[READ]`/`[WRITE]` tag in the
77
+ > tool's own documentation — so a write can't quietly present itself as a read.
78
+
79
+ Running a smaller / local model? See
80
+ [agent-guardrails.md](skills/proxy-aiops/references/agent-guardrails.md) — it lists
81
+ the guardrails this tool now enforces for you (so you don't spend prompt budget
82
+ restating them) and gives a ready-made system prompt for what's left.
83
+
84
+ ## Tool inventory (28 tools)
45
85
 
46
86
  | Domain | Tools | # | Kind |
47
87
  |--------|-------|:-:|------|
@@ -55,6 +95,7 @@ risk-tier approval, undo-token recording, and output sanitisation.
55
95
  | **Writes (caddy)** | `set_config_value` | 1 | write (**med**) |
56
96
  | **Writes (caddy)** | `delete_config_path`, `load_config` | 2 | write (**high**) |
57
97
  | **Writes (haproxy)** | `set_server_state`, `set_server_weight` | 2 | write (**med**) |
98
+ | **Undo** | `undo_list`, `undo_apply` | 2 | read / write |
58
99
 
59
100
  Reversible writes record an inverse **undo descriptor** built from the real fetched
60
101
  before-state (`set_config_value` restores the prior subtree; `delete_config_path`
@@ -175,17 +216,20 @@ Every MCP tool is wrapped by `@governed_tool`:
175
216
  - **Sanitisation** — all proxy-returned text is bounded + control-character
176
217
  sanitised before it reaches the agent.
177
218
 
178
- ## Preview status
219
+ ## Platform support & verification status
179
220
 
180
221
  - **Platforms**: Traefik (API + /metrics), Caddy (admin API), HAProxy (Data Plane
181
222
  API v2).
182
- - **Preview mock-validated only. Not run against a live proxy.** All behaviour is
183
- validated against mocked JSON/metrics responses; the concrete REST paths are
184
- modelled from each project's public API and need live verification. All three
185
- platforms are free and self-hostable (a small container-compose lab with
186
- traefik + caddy + haproxy/dataplaneapi is a one-evening setup), so
187
- `proxy-aiops doctor` a health/info probe per platform is the fastest
188
- live check.
223
+ - **Test coverage**: behaviour is validated against mocked JSON/metrics responses
224
+ every module imports, every MCP tool carries the governance marker, the four flagship
225
+ analyses are unit-tested against synthetic telemetry, the support matrix is asserted
226
+ to raise teaching errors (never a silent no-op), and reversible writes are asserted to
227
+ record the correct inverse undo descriptor. The concrete REST paths are modelled from
228
+ each project's public API and have not yet been exercised against a live proxy. See
229
+ [docs/VERIFICATION.md](docs/VERIFICATION.md) for the checklist a live run must
230
+ satisfy. All three platforms are free and self-hostable (a small container-compose lab
231
+ with traefik + caddy + haproxy/dataplaneapi is a one-evening setup), and
232
+ `proxy-aiops doctor` — a health/info probe per platform — is the fastest live check.
189
233
  - **Routing note**: this tool operates reverse proxies / load balancers. Do NOT
190
234
  use it for firewall rules — use firewall-aiops.
191
235
  - **Missing a capability?** Open an issue or PR at
@@ -0,0 +1,48 @@
1
+ # Release notes — proxy-aiops 0.2.0
2
+
3
+ Previous release: 0.1.0.
4
+
5
+ ## Headline: read-only mode
6
+
7
+ ```bash
8
+ export PROXY_READ_ONLY=1
9
+ ```
10
+
11
+ With this set the **6 write tools are never registered** — an MCP
12
+ client lists **22 tools instead of 28**. The writes are not hidden
13
+ behind a flag and not merely refused on call: they are absent from the session,
14
+ so a model cannot invoke one and cannot be argued into one. For a reviewer this
15
+ is checkable rather than promised — connect, list the tools, and the writes are
16
+ not there.
17
+
18
+ Enforcement is two layers deep: the `@governed_tool` harness refuses every
19
+ non-read operation (covering the CLI and in-process callers too), and the MCP
20
+ server removes write tools from `list_tools()`. Changing entry point does not
21
+ get around it.
22
+
23
+ ## BREAKING — return shapes changed
24
+
25
+ This release changes payloads that callers may be parsing. Both changes exist
26
+ to stop a result from misrepresenting itself:
27
+
28
+ 1. **Absent fields are now `null`, not `""`.** A missing value and an empty value
29
+ were previously indistinguishable, which invited consumers to invent the
30
+ difference. Keys are still always present — only the value may be null.
31
+ 2. **Anything with a `limit` now returns an envelope** —
32
+ `{"<items>": [...], "returned": N, "limit": L, "truncated": bool}`. Truncation is
33
+ *measured* (one extra row is fetched), never inferred from the page happening to
34
+ be full. Where a genuine pre-cap total is knowable it is reported as `total`;
35
+ where it isn't, `total` is deliberately omitted rather than echoing `returned`.
36
+
37
+ ## Also in this release
38
+
39
+ - **`docs/VERIFICATION.md`** — what the mock suite actually guarantees, a live
40
+ verification checklist, and the criteria for claiming this tool verified.
41
+ - **`skills/proxy-aiops/references/agent-guardrails.md`** — for driving this tool with a
42
+ smaller / local model: which guardrails are now enforced for you, and a
43
+ ready-made system prompt for the rest.
44
+ - Expanded operator playbooks in the skill documentation.
45
+ - The advertised tool count now matches what an MCP client actually lists
46
+ (it includes `undo_list` / `undo_apply`), and a release gate keeps it honest.
47
+ - The `(preview)` label has been dropped. It never meant unreleased; verification
48
+ status now lives in `docs/VERIFICATION.md` where it can be specific.
@@ -0,0 +1,191 @@
1
+ # Live verification — Traefik / Caddy / HAProxy
2
+
3
+ `proxy-aiops` is exercised by a **mock-only** test suite (`uv run pytest`, no real
4
+ proxy). It has **not** yet been validated end-to-end against a live Traefik, Caddy, or
5
+ HAProxy instance. This document says exactly what the mock suite already guarantees, and
6
+ what a live run has to prove before anyone may describe this tool as verified against a
7
+ real proxy.
8
+
9
+ It is deliberately checklist-shaped so the result is reproducible and auditable — not a
10
+ subjective "seems fine".
11
+
12
+ ## What the mock suite already guarantees
13
+
14
+ - Every module imports; the CLI builds; **all 28 MCP tools** carry the `@governed_tool`
15
+ harness marker (`tests/test_smoke.py`, which also asserts the tool count and that
16
+ `__version__` matches `pyproject.toml`).
17
+ - The four flagship analyses (`backend_health_rca`, `cert_expiry_sweep`,
18
+ `error_rate_rca`, `route_conflict_analysis`) are unit-tested against synthetic
19
+ telemetry: the health-check failure classes (connection refused / L4 timeout / TLS /
20
+ L7 / DNS / maint) each map to the right cause, the dominant-status-code mapping
21
+ (502/503/504/500) fires correctly, expiry bucketing respects the warn/critical
22
+ thresholds, and shadowed / dead / looping routes are each detected with the covering
23
+ route or missing service named.
24
+ - The **platform registry** resolves each tool to the correct Traefik (`/api/...` plus
25
+ the `/metrics` text format), Caddy (admin API), and HAProxy (Data Plane API v2
26
+ `/v2/...`, HTTP Basic) request shape.
27
+ - The **support matrix** is asserted to raise a *teaching error* — never a silent no-op —
28
+ for operations a platform cannot perform (Traefik writes → edit its providers; Caddy
29
+ error counters → access logs; HAProxy certs → the `.pem` pipeline).
30
+ - Reversible writes record a faithful **inverse** undo descriptor built from a fetched
31
+ before-state (`set_config_value` restores the prior subtree, or deletes a path it
32
+ created; `delete_config_path` re-creates the captured subtree; `set_server_state` /
33
+ `set_server_weight` restore the prior admin state / weight), and the descriptor's
34
+ params replay against the tool's own signature.
35
+ - Governance persistence is tested against a real on-disk SQLite audit DB: calls land as
36
+ rows, failures record `status=error` and no undo, and the secure-by-default approver
37
+ gate refuses high-risk ops when no `rules.yaml` exists.
38
+
39
+ What it does **not** guarantee: that the concrete REST paths, JSON field names, and the
40
+ Traefik `/metrics` text format match a real build of each proxy. Those are modelled from
41
+ each project's public API documentation and are the **largest verification debt in this
42
+ repo** — the metrics-text parsing in particular is brittle against format changes.
43
+
44
+ ## Prerequisites for a live run
45
+
46
+ All three proxies are free and self-hostable; a single compose file gets you the whole
47
+ matrix in an evening:
48
+
49
+ - **Traefik** — expose the API (`--api=true`, a router on `api@internal`) **and**
50
+ metrics (`--metrics.prometheus=true`), since `traffic_stats` / `error_counters` read
51
+ the metrics endpoint, not the API.
52
+ - **Caddy** — the admin API on `localhost:2019` (its default). This is the target that
53
+ carries the config write surface, so it is where sections 4–6 actually run.
54
+ - **HAProxy** — plus the **Data Plane API v2** (`dataplaneapi`), with a Basic-auth user.
55
+ This is the target that carries the runtime server writes.
56
+
57
+ Behind each proxy, put **at least two throwaway backends** so you can drain one, and a
58
+ deliberately broken one (a closed port, and a TLS-mismatched one) so the health RCA has
59
+ real failures to classify. Use a **lab edge** — the checklist drains servers and rewrites
60
+ config, and `load_config` replaces a Caddy instance's *entire* configuration.
61
+
62
+ ```bash
63
+ uv tool install proxy-aiops
64
+ proxy-aiops init # wizard: pick platform, optional encrypted secret
65
+ ```
66
+
67
+ Record the exact versions tested (e.g. "Traefik 3.2, Caddy 2.8, HAProxy 3.0 +
68
+ dataplaneapi 3.0") — a tick is only meaningful with the build it was ticked against.
69
+
70
+ ## Verification checklist
71
+
72
+ Tick every box, **per platform**. A box that cannot be ticked is a verification gap —
73
+ record it, do not silently pass.
74
+
75
+ ### 1. Connectivity (the fastest live gate)
76
+ - [ ] `proxy-aiops doctor` → green on each target: config parsed, secret store unlocks
77
+ (HAProxy), and a real health/info probe returns.
78
+ - [ ] `proxy-aiops doctor --skip-auth` → passes offline (config/secret checks only).
79
+ - [ ] One config spanning **all three** platforms works with `--target` switching — this
80
+ mixed-edge case is the reason the platform registry exists.
81
+
82
+ ### 2. Reads return real, well-shaped data
83
+ - [ ] `proxy-aiops overview` → real platform/version, route and service counts, and
84
+ upstream up/down totals matching each proxy's own dashboard.
85
+ - [ ] `proxy-aiops routes list` → the real routers/routes/frontends with correct hosts,
86
+ paths, priority, and TLS flags.
87
+ - [ ] `proxy-aiops routes show <name>` → one route's full detail.
88
+ - [ ] `proxy-aiops routes find <host> --path /api` → returns the route that **actually**
89
+ serves that request. Verify by curling the host and comparing which backend
90
+ answers — this is the single most valuable correctness check in the file.
91
+ - [ ] `proxy-aiops services list` / `services show <name>` / `services upstreams <name>`
92
+ → real servers with correct address, status (up/down/maint/drain), check info, and
93
+ weight.
94
+ - [ ] MCP `list_middlewares` → real Traefik middlewares; a **teaching error** on Caddy
95
+ and HAProxy rather than an empty list.
96
+ - [ ] MCP `traffic_stats` / `error_counters` → numbers that move when you generate load,
97
+ and that agree with the proxy's own stats page. On Traefik confirm the
98
+ **`/metrics` text parsing** survives the real format; on Caddy confirm the
99
+ teaching error (counters come from access logs there).
100
+ - [ ] `proxy-aiops config snapshot` / `config search <needle>` / `config get <path>` →
101
+ the real Caddy config tree; teaching errors on Traefik/HAProxy.
102
+ - [ ] `proxy-aiops certs` → the real TLS domain inventory; teaching note on HAProxy.
103
+
104
+ ### 3. The analyses are right, not just non-crashing
105
+ - [ ] Stop one backend; `proxy-aiops analyze health` names that server and classifies the
106
+ failure as connection-refused (not a generic "down").
107
+ - [ ] Point a backend at a TLS-mismatched endpoint; the class comes back TLS, not L4.
108
+ - [ ] Drive real 503s (all servers of one service down) and real 504s (a backend that
109
+ sleeps past the timeout); `proxy-aiops analyze errors --rate 5 --min-requests 100`
110
+ flags the right service and maps each **dominant code to the correct cause**.
111
+ - [ ] Confirm `--min-requests` actually suppresses a low-traffic service with a single
112
+ failure.
113
+ - [ ] `proxy-aiops certs --sweep --warn-days 30 --critical-days 7` → live-probed expiry
114
+ dates match `openssl s_client` for the same domain, and bucketing is correct
115
+ either side of both thresholds. Repeat with `--port` on a non-443 listener.
116
+ - [ ] Create a genuinely shadowed route (a broad higher-priority route above a specific
117
+ one) and a dead route (pointing at a nonexistent service);
118
+ `proxy-aiops analyze conflicts` names the covering route and the missing service.
119
+
120
+ ### 4. A reversible write + its undo — Caddy config
121
+ - [ ] `proxy-aiops config set <path> '<json>' --dry-run` → prints the write, changes
122
+ nothing (confirm with `config get <path>`).
123
+ - [ ] `proxy-aiops config set <path> '<json>'` → the config actually changes, the result
124
+ carries an `_undo_id`, and a row lands in `~/.proxy-aiops/audit.db`.
125
+ - [ ] `proxy-aiops undo list` shows it; `undo apply <id>` restores the **prior subtree
126
+ byte-for-byte** (compare against the `config snapshot` you took first).
127
+ - [ ] `config set` on a path that did **not** previously exist, then `undo apply` → the
128
+ created path is *deleted*, not reset to a guessed value. This asymmetric case is
129
+ where a naive undo silently leaves debris.
130
+ - [ ] MCP `delete_config_path` then `undo apply` → the deleted subtree is re-created
131
+ intact.
132
+
133
+ ### 5. A reversible write + its undo — HAProxy runtime
134
+ - [ ] `proxy-aiops server state <backend> <server> drain --dry-run` → nothing changes.
135
+ - [ ] `proxy-aiops server state <backend> <server> drain` → the server actually drains
136
+ (confirm in the HAProxy stats page), in-flight connections finish, and an
137
+ `_undo_id` is returned.
138
+ - [ ] `undo apply <id>` restores the **prior** state — verify starting from a server that
139
+ was already in `maint`, where a naive undo would wrongly set `ready`.
140
+ - [ ] `proxy-aiops server weight <backend> <server> 50` then `undo apply` → the prior
141
+ weight returns, not a default.
142
+ - [ ] The same commands against a Traefik or Caddy target raise a **teaching error**
143
+ naming the right mechanism.
144
+
145
+ ### 6. Governance actually gates
146
+ - [ ] With no `~/.proxy-aiops/rules.yaml`, `delete_config_path` and `load_config` are
147
+ **refused** unless `PROXY_AUDIT_APPROVED_BY` is set (secure-by-default); with it
148
+ plus `PROXY_AUDIT_RATIONALE`, both appear in the audit row.
149
+ - [ ] `load_config` on a lab Caddy replaces the whole config, and `undo apply` restores
150
+ the snapshotted original — then confirm traffic actually flows again, since a
151
+ restored-but-not-reloaded config would still be an outage.
152
+ - [ ] A tight poll loop trips the runaway budget guard rather than hammering the API.
153
+ - [ ] A failed call (nonexistent route name) is audited `status=error` with no undo.
154
+
155
+ ### 7. Cleanup
156
+ - [ ] Return every drained server to its prior state, restore every weight, and undo
157
+ every config edit.
158
+ - [ ] `proxy-aiops overview` and `proxy-aiops config snapshot` match the baseline you
159
+ captured before starting.
160
+ - [ ] Skim `~/.proxy-aiops/audit.db` — every write is there with the right risk tier.
161
+
162
+ ## Criteria to consider it live-verified
163
+
164
+ All of the following must hold:
165
+
166
+ 1. Every box above is ticked against **all three** platforms, with the exact builds
167
+ recorded (e.g. "Traefik 3.2 + Caddy 2.8 + HAProxy 3.0/dataplaneapi 3.0").
168
+ 2. Every REST-path, field-shape, or metrics-format mismatch found is **fixed and covered
169
+ by a regression test**, so the mock suite would now catch it.
170
+ 3. Sections 4 and 5 passed, including the asymmetric cases (creating a config path then
171
+ undoing it; undoing a drain on a server that started in `maint`). Recording an undo
172
+ descriptor is not the same as the undo working, and this product line has shipped
173
+ broken undo pairs before.
174
+ 4. Every support-matrix teaching error was confirmed **live** — a real unsupported call
175
+ must teach, not silently succeed or silently do nothing.
176
+ 5. The run is written up in the release notes / product-line memory with the date and
177
+ package version, matching how the line records its other live-verified tools.
178
+
179
+ Until then, this repo says only what is true: mock-validated, live-unverified. Claiming
180
+ otherwise would break that promise.
181
+
182
+ ## Notes for maintainers
183
+
184
+ - `proxy-aiops doctor` is the single fastest live entry point; start there.
185
+ - Weight the run toward **Traefik `/metrics` text parsing** and the **Caddy config-path
186
+ semantics** — those are the two places the mocks are least able to model, and both
187
+ change between releases.
188
+ - Managed cloud load balancers are explicitly out of scope; do not infer support for them
189
+ from a self-hosted run.
190
+ - Add this tool's result to the product-line verification ledger once green, so the
191
+ central "verification debt" list stays accurate.
@@ -77,7 +77,7 @@ def tool_errors(shape: str = "dict") -> Callable:
77
77
  mcp = FastMCP(
78
78
  "proxy-aiops",
79
79
  instructions=(
80
- "Reverse-proxy / load-balancer operations (preview) over Traefik, Caddy "
80
+ "Reverse-proxy / load-balancer operations over Traefik, Caddy "
81
81
  "and HAProxy: version/entrypoints, routes (routers / caddy routes / "
82
82
  "frontends), services and server-level upstream health, middlewares, "
83
83
  "TLS certificate inventory, traffic/error counters, and config "
@@ -0,0 +1,78 @@
1
+ """MCP server wrapping proxy-aiops operations (stdio transport).
2
+
3
+ Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
4
+ delegates to the ``proxy_aiops`` ops package and is wrapped with the
5
+ proxy-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
6
+
7
+ Standalone, self-governed reverse-proxy operations over Traefik,
8
+ Caddy and HAProxy: status/routes/services/upstreams/certs/traffic/config
9
+ reads, four flagship analyses, and governed writes (caddy config set/delete/
10
+ load, haproxy server state/weight).
11
+
12
+ Source: https://github.com/AIops-tools/Proxy-AIops
13
+ License: MIT
14
+ """
15
+
16
+ import logging
17
+
18
+ from mcp_server._shared import _safe_error, mcp, tool_errors
19
+
20
+ # Importing the tool modules registers every @mcp.tool() onto the shared
21
+ # `mcp` instance. Order does not matter; each module is self-contained.
22
+ from mcp_server.tools import ( # noqa: F401 — side effects
23
+ analysis,
24
+ certs,
25
+ configread,
26
+ routes,
27
+ services,
28
+ status,
29
+ traffic,
30
+ undo,
31
+ writes,
32
+ )
33
+ from proxy_aiops.governance import READ_ONLY_ENV, is_read_only
34
+
35
+ __all__ = ["mcp", "main", "_safe_error", "tool_errors", "apply_read_only"]
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def apply_read_only() -> list[str]:
41
+ """Unregister every write tool when read-only mode is on.
42
+
43
+ The ``@governed_tool`` harness already refuses writes in this mode, so this
44
+ is a second layer — and the one that matters for smaller local models:
45
+ a tool absent from ``list_tools()`` cannot be hallucinated into a call,
46
+ whereas a tool that exists but refuses invites retry loops. It also gives a
47
+ compliance reviewer something checkable — the write tools are simply not
48
+ exposed.
49
+
50
+ ``risk_level == "low"`` is the read/write discriminator; a smoke test
51
+ asserts it stays in agreement with each tool's ``[READ]``/``[WRITE]``
52
+ docstring tag so the two can never drift apart silently.
53
+
54
+ Returns the names that were removed (empty when not in read-only mode).
55
+ """
56
+ if not is_read_only():
57
+ return []
58
+ registry = mcp._tool_manager._tools
59
+ dropped = [
60
+ name
61
+ for name, tool in registry.items()
62
+ if getattr(getattr(tool, "fn", None), "_risk_level", "low") != "low"
63
+ ]
64
+ for name in dropped:
65
+ del registry[name]
66
+ if dropped:
67
+ logger.info(
68
+ "%s is set — read-only mode: %d write tool(s) not exposed",
69
+ READ_ONLY_ENV, len(dropped),
70
+ )
71
+ return dropped
72
+
73
+
74
+ def main() -> None:
75
+ """Run the MCP server over stdio."""
76
+ logging.basicConfig(level=logging.INFO)
77
+ apply_read_only()
78
+ mcp.run(transport="stdio")
@@ -31,6 +31,10 @@ def search_config(query: str, target: Optional[str] = None) -> dict:
31
31
  Args:
32
32
  query: Case-insensitive substring to find in keys and values.
33
33
  target: Proxy target name from config; omit for the default.
34
+
35
+ Returns an envelope: {"matches": [...], "returned": N, "limit": L,
36
+ "truncated": bool}. When "truncated" is true there is more than was
37
+ returned — narrow the query rather than treating these as every match.
34
38
  """
35
39
  return ops.search_config(_get_connection(target), query)
36
40
 
@@ -17,7 +17,12 @@ def traffic_stats(target: Optional[str] = None) -> dict:
17
17
 
18
18
  Args:
19
19
  target: Proxy target name from config; omit for the default.
20
+
21
+ Returns an envelope: {"services": [...], "returned": N, "limit": L,
22
+ "truncated": bool, "total": T}. When "truncated" is true this proxy serves
23
+ more services than were returned.
20
24
  """
25
+
21
26
  return ops.traffic_stats(_get_connection(target))
22
27
 
23
28
 
@@ -32,5 +37,10 @@ def error_counters(target: Optional[str] = None) -> dict:
32
37
 
33
38
  Args:
34
39
  target: Proxy target name from config; omit for the default.
40
+
41
+ Returns an envelope: {"services": [...], "returned": N, "limit": L,
42
+ "truncated": bool, "total": T}. When "truncated" is true this proxy serves
43
+ more services than were returned (only the busiest services are returned).
35
44
  """
45
+
36
46
  return ops.error_counters(_get_connection(target))
@@ -36,13 +36,24 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
36
36
  Each entry names the original tool, the inverse tool that ``undo_apply``
37
37
  would run, and a human note. Use the ``undoId`` with ``undo_apply``.
38
38
 
39
+ Returns {"undos": [...], "returned": N, "limit": L, "truncated": bool}.
40
+ ``truncated`` is *measured* (one extra row is fetched), not guessed from a
41
+ length coincidence: when it is true there are MORE tokens than shown, so
42
+ re-run with a higher ``limit`` rather than reporting the list as complete.
43
+
39
44
  Args:
40
- limit: Max rows to return (default 50).
45
+ limit: Max rows to return (default 50, capped at 500).
41
46
  target: Unused (undo state is host-local); accepted for CLI uniformity.
42
47
  """
43
- rows = get_undo_store().list(status="recorded", limit=max(1, min(limit, 500)))
48
+ requested = max(1, min(limit, 500))
49
+ raw = get_undo_store().list(status="recorded", limit=requested + 1)
50
+ truncated = len(raw) > requested
51
+ rows = raw[:requested]
44
52
  return {
45
53
  "count": len(rows),
54
+ "returned": len(rows),
55
+ "limit": requested,
56
+ "truncated": truncated,
46
57
  "undos": [
47
58
  {
48
59
  "undoId": r["undo_id"],
@@ -50,7 +50,13 @@ def config_search(
50
50
  from proxy_aiops.ops import configread as ops
51
51
 
52
52
  conn, _ = get_connection(target)
53
- console.print_json(json.dumps(ops.search_config(conn, query)))
53
+ result = ops.search_config(conn, query)
54
+ console.print_json(json.dumps(result))
55
+ if result.get("truncated"):
56
+ console.print(
57
+ f"[yellow]… truncated at {result.get('limit')} matches — "
58
+ f"narrow the query to see the rest.[/yellow]"
59
+ )
54
60
 
55
61
 
56
62
  @config_app.command("get")
@@ -18,12 +18,16 @@ from proxy_aiops.governance.budget import BudgetExceeded, BudgetTracker, get_bud
18
18
  from proxy_aiops.governance.decorators import PolicyDenied, governed_tool
19
19
  from proxy_aiops.governance.patterns import Pattern, PatternMatch, get_pattern_engine
20
20
  from proxy_aiops.governance.policy import TierDecision, get_policy_engine
21
- from proxy_aiops.governance.sanitize import sanitize
21
+ from proxy_aiops.governance.readonly import READ_ONLY_ENV, is_read_only
22
+ from proxy_aiops.governance.sanitize import opt_str, sanitize
22
23
  from proxy_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 proxy_aiops.governance.audit import detect_agent, get_engine
36
36
  from proxy_aiops.governance.budget import BudgetExceeded, get_budget
37
37
  from proxy_aiops.governance.patterns import PatternMatch, get_pattern_engine
38
38
  from proxy_aiops.governance.policy import PolicyResult, get_policy_engine
39
+ from proxy_aiops.governance.readonly import is_read_only, read_only_denial
39
40
  from proxy_aiops.governance.sanitize import sanitize
40
41
 
41
42
  _log = logging.getLogger("proxy-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,