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.
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/PKG-INFO +55 -11
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/README.md +54 -10
- proxy_aiops-0.2.0/RELEASE_NOTES.md +48 -0
- proxy_aiops-0.2.0/docs/VERIFICATION.md +191 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/_shared.py +1 -1
- proxy_aiops-0.2.0/mcp_server/server.py +78 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/configread.py +4 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/traffic.py +10 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/undo.py +13 -2
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/configcmd.py +7 -1
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/__init__.py +5 -1
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/decorators.py +12 -0
- proxy_aiops-0.2.0/proxy_aiops/governance/readonly.py +49 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/sanitize.py +25 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/_util.py +18 -1
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/analysis.py +16 -16
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/certs.py +2 -2
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/configread.py +13 -4
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/routes.py +16 -14
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/services.py +18 -18
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/status.py +11 -11
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/traffic.py +13 -5
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/pyproject.toml +1 -1
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/server.json +3 -3
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/SKILL.md +104 -39
- proxy_aiops-0.2.0/skills/proxy-aiops/references/agent-guardrails.md +106 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/capabilities.md +8 -1
- proxy_aiops-0.2.0/tests/test_cli_reads.py +240 -0
- proxy_aiops-0.2.0/tests/test_cli_secret.py +139 -0
- proxy_aiops-0.2.0/tests/test_connection.py +222 -0
- proxy_aiops-0.2.0/tests/test_gov_audit.py +292 -0
- proxy_aiops-0.2.0/tests/test_gov_decorators.py +452 -0
- proxy_aiops-0.2.0/tests/test_gov_patterns.py +417 -0
- proxy_aiops-0.2.0/tests/test_gov_policy.py +415 -0
- proxy_aiops-0.2.0/tests/test_mcp_read_tools.py +162 -0
- proxy_aiops-0.2.0/tests/test_ops_extra.py +238 -0
- proxy_aiops-0.2.0/tests/test_optional_fields.py +202 -0
- proxy_aiops-0.2.0/tests/test_readonly.py +142 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/uv.lock +1 -1
- proxy_aiops-0.1.0/RELEASE_NOTES.md +0 -61
- proxy_aiops-0.1.0/mcp_server/server.py +0 -40
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.github/workflows/mcp-publish.yml +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.github/workflows/publish.yml +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/.gitignore +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/CHANGELOG.md +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/LICENSE +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/SECURITY.md +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/__init__.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/__init__.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/analysis.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/certs.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/routes.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/services.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/status.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/mcp_server/tools/writes.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/__init__.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/__init__.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/_common.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/_root.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/analyze.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/certs.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/doctor.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/init.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/overview.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/routes.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/secret.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/server.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/services.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/cli/undo.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/config.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/connection.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/doctor.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/audit.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/budget.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/paths.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/patterns.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/policy.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/governance/undo.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/__init__.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/overview.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/ops/writes.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/platform.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/proxy_aiops/secretstore.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/cli-reference.md +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/skills/proxy-aiops/references/setup-guide.md +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/smithery.yaml +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/conftest.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_analysis.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_cli_writes.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_doctor.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_governance_persistence.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_init.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_platform.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_reads.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_secretstore.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_smoke.py +0 -0
- {proxy_aiops-0.1.0 → proxy_aiops-0.2.0}/tests/test_undo_executor.py +0 -0
- {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.
|
|
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
|
|
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
|
-
##
|
|
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
|
-
##
|
|
235
|
+
## Platform support & verification status
|
|
195
236
|
|
|
196
237
|
- **Platforms**: Traefik (API + /metrics), Caddy (admin API), HAProxy (Data Plane
|
|
197
238
|
API v2).
|
|
198
|
-
- **
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
live
|
|
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
|
|
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
|
-
##
|
|
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
|
-
##
|
|
219
|
+
## Platform support & verification status
|
|
179
220
|
|
|
180
221
|
- **Platforms**: Traefik (API + /metrics), Caddy (admin API), HAProxy (Data Plane
|
|
181
222
|
API v2).
|
|
182
|
-
- **
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
live
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|