proxy-aiops 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/PKG-INFO +1 -1
- proxy_aiops-0.3.1/RELEASE_NOTES.md +26 -0
- proxy_aiops-0.3.1/docs/VERIFICATION.md +36 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/connection.py +23 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/_util.py +24 -1
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/routes.py +12 -4
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/services.py +5 -5
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/platform.py +18 -13
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/pyproject.toml +1 -1
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/server.json +2 -2
- proxy_aiops-0.3.1/tests/test_dpapi_version.py +56 -0
- proxy_aiops-0.3.1/tests/test_int_fidelity.py +34 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_platform.py +4 -2
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/uv.lock +1 -1
- proxy_aiops-0.2.0/RELEASE_NOTES.md +0 -48
- proxy_aiops-0.2.0/docs/VERIFICATION.md +0 -191
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/.github/workflows/mcp-publish.yml +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/.github/workflows/publish.yml +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/.gitignore +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/CHANGELOG.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/LICENSE +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/README.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/SECURITY.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/_shared.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/server.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/analysis.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/certs.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/configread.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/routes.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/services.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/status.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/traffic.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/undo.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/mcp_server/tools/writes.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/_common.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/_root.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/analyze.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/certs.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/configcmd.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/doctor.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/init.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/overview.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/routes.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/secret.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/server.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/services.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/cli/undo.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/config.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/doctor.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/audit.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/budget.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/decorators.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/paths.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/patterns.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/policy.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/readonly.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/sanitize.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/governance/undo.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/__init__.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/analysis.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/certs.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/configread.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/overview.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/status.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/traffic.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/ops/writes.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/proxy_aiops/secretstore.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/skills/proxy-aiops/SKILL.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/skills/proxy-aiops/references/agent-guardrails.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/skills/proxy-aiops/references/capabilities.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/skills/proxy-aiops/references/cli-reference.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/skills/proxy-aiops/references/setup-guide.md +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/smithery.yaml +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/conftest.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_analysis.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_cli_reads.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_cli_secret.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_cli_writes.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_connection.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_doctor.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_gov_audit.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_gov_decorators.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_gov_patterns.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_gov_policy.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_governance_persistence.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_init.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_mcp_read_tools.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_ops_extra.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_optional_fields.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_readonly.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_reads.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_secretstore.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_smoke.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/tests/test_undo_executor.py +0 -0
- {proxy_aiops-0.2.0 → proxy_aiops-0.3.1}/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.3.1
|
|
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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Release notes — proxy-aiops 0.3.1
|
|
2
|
+
|
|
3
|
+
Previous release: 0.3.0.
|
|
4
|
+
|
|
5
|
+
## Fixed: the HAProxy branch could not talk to any current HAProxy
|
|
6
|
+
|
|
7
|
+
Every HAProxy path was hardcoded to Data Plane API **v2**. HAProxy 3.x ships
|
|
8
|
+
Data Plane API v3, which serves **only** `/v3` — every `/v2` path returns 404.
|
|
9
|
+
So the whole HAProxy platform branch was unusable against a current HAProxy,
|
|
10
|
+
failing at the very first probe.
|
|
11
|
+
|
|
12
|
+
The path registry now holds v3 paths, and the connection probes `/v3/info` once
|
|
13
|
+
per connection and rewrites the prefix to `/v2` when it sees an older server.
|
|
14
|
+
Both generations work; the probe is cached, so a v3 server costs one extra
|
|
15
|
+
request per connection and a v2 server two.
|
|
16
|
+
|
|
17
|
+
## Live-verified: HAProxy
|
|
18
|
+
|
|
19
|
+
Verified against **HAProxy 3.0.25 with Data Plane API v3.0.21**: `doctor`, plus
|
|
20
|
+
reads cross-checked against the Data Plane API itself — the configured backend
|
|
21
|
+
and its two servers were reported accurately, including `serversUp: 0` for
|
|
22
|
+
servers pointed at closed ports.
|
|
23
|
+
|
|
24
|
+
With Traefik 3.2.5 and Caddy 2 verified in 0.3.0, **all three platforms are now
|
|
25
|
+
live-verified**. See [docs/VERIFICATION.md](docs/VERIFICATION.md) — guarded
|
|
26
|
+
config writes and TLS/certificate expiry against real certificates remain open.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Live verification status
|
|
2
|
+
|
|
3
|
+
This document records what has and has not been validated against real reverse
|
|
4
|
+
proxies, so the maturity claim is auditable rather than a vibe.
|
|
5
|
+
|
|
6
|
+
## Already live-verified ✅ — all three platforms (2026-07-20)
|
|
7
|
+
|
|
8
|
+
Traefik 3.2.5, Caddy 2, and HAProxy 3.0.25 (Data Plane API v3.0.21).
|
|
9
|
+
|
|
10
|
+
- `doctor` against both live endpoints (unauthenticated admin APIs, as the tool
|
|
11
|
+
documents for traefik/caddy).
|
|
12
|
+
- Reads cross-checked against each proxy's own API: `overview`, `routes list`,
|
|
13
|
+
`services list`, and the three analyses (`analyze health/errors/conflicts`).
|
|
14
|
+
Router counts and names matched `/api/http/routers` (Traefik) and
|
|
15
|
+
`/config/apps/http/servers/...` (Caddy) exactly.
|
|
16
|
+
|
|
17
|
+
**A real bug was found and fixed by this run**: route priority is an int64 in
|
|
18
|
+
Traefik, and routing it through the float helper both rendered it in scientific
|
|
19
|
+
notation (`9.223372036854776e+18`) *and* lost precision. Two routers with
|
|
20
|
+
**different** priorities (…806 and …805) displayed as the **same** value. Route
|
|
21
|
+
priority decides matching order, so collapsing distinct values actively misleads
|
|
22
|
+
anyone debugging route precedence. Integer quantities now use an exact `as_int`
|
|
23
|
+
that never round-trips through float64.
|
|
24
|
+
|
|
25
|
+
## Not yet live-verified ⚠️
|
|
26
|
+
|
|
27
|
+
- **HAProxy is now verified** — and that run found the branch was **entirely
|
|
28
|
+
broken**: every path was hardcoded to Data Plane API v2, but HAProxy 3.x serves
|
|
29
|
+
only `/v3`, so the first probe 404'd. The connection now detects the API
|
|
30
|
+
generation and supports both. Still untested there: runtime server state changes
|
|
31
|
+
and the stats-derived traffic analyses under real load.
|
|
32
|
+
- **TLS / certificate expiry** (`certs`) against real certificates — both verified
|
|
33
|
+
instances served plaintext on a lab port.
|
|
34
|
+
- **Guarded config writes** (`config set/delete`) and their undo paths.
|
|
35
|
+
- Traffic/error-rate analyses against a proxy under real load (the lab instances
|
|
36
|
+
served no traffic, so `analyze errors` had nothing to rank).
|
|
@@ -84,6 +84,8 @@ class ProxyConnection:
|
|
|
84
84
|
auth=self._build_auth(target),
|
|
85
85
|
headers={"Accept": "application/json"},
|
|
86
86
|
)
|
|
87
|
+
# HAProxy Data Plane API generation, resolved lazily by _haproxy_path.
|
|
88
|
+
self._dpapi_prefix: str | None = None
|
|
87
89
|
|
|
88
90
|
@staticmethod
|
|
89
91
|
def _build_auth(target: TargetConfig) -> Any | None:
|
|
@@ -104,8 +106,29 @@ class ProxyConnection:
|
|
|
104
106
|
def platform(self) -> Any:
|
|
105
107
|
return self._target.platform_obj
|
|
106
108
|
|
|
109
|
+
def _haproxy_path(self, path: str) -> str:
|
|
110
|
+
"""Rewrite a ``/v3/`` Data Plane API path to ``/v2/`` on an older server.
|
|
111
|
+
|
|
112
|
+
HAProxy 3.x ships Data Plane API v3, which serves **only** ``/v3`` — every
|
|
113
|
+
``/v2`` path 404s. Older installs are the mirror image. The registry holds
|
|
114
|
+
the current (v3) paths and this probes ``/v3/info`` once per connection,
|
|
115
|
+
falling back to ``/v2`` so both generations work. Probed lazily and cached:
|
|
116
|
+
a v3 server costs one extra request per connection, a v2 server two.
|
|
117
|
+
"""
|
|
118
|
+
if not path.startswith("/v3/"):
|
|
119
|
+
return path
|
|
120
|
+
if self._dpapi_prefix is None:
|
|
121
|
+
try:
|
|
122
|
+
resp = self._client.request("GET", "/v3/info")
|
|
123
|
+
self._dpapi_prefix = "/v3/" if resp.status_code < 400 else "/v2/"
|
|
124
|
+
except httpx.HTTPError:
|
|
125
|
+
self._dpapi_prefix = "/v3/" # unreachable: let the real call report it
|
|
126
|
+
return self._dpapi_prefix + path[4:]
|
|
127
|
+
|
|
107
128
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
108
129
|
label = self._target.platform_obj.label
|
|
130
|
+
if self._target.platform == "haproxy":
|
|
131
|
+
path = self._haproxy_path(path)
|
|
109
132
|
try:
|
|
110
133
|
resp = self._client.request(method, path, **kwargs)
|
|
111
134
|
except httpx.HTTPError as exc:
|
|
@@ -71,13 +71,36 @@ def to_bool(value: Any) -> bool:
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def num(value: Any) -> float:
|
|
74
|
-
"""Coerce a numeric cell to float; 0.0 when absent/non-numeric.
|
|
74
|
+
"""Coerce a numeric cell to float; 0.0 when absent/non-numeric.
|
|
75
|
+
|
|
76
|
+
Use only for genuinely fractional values (rates, durations). For integer
|
|
77
|
+
quantities — request counts, session counts, route priority — use
|
|
78
|
+
:func:`as_int`.
|
|
79
|
+
"""
|
|
75
80
|
try:
|
|
76
81
|
return float(value)
|
|
77
82
|
except (TypeError, ValueError):
|
|
78
83
|
return 0.0
|
|
79
84
|
|
|
80
85
|
|
|
86
|
+
def as_int(value: Any) -> int:
|
|
87
|
+
"""Coerce an integer quantity to ``int``; 0 when absent/non-numeric.
|
|
88
|
+
|
|
89
|
+
Route priority is an int64 in Traefik: routing it through :func:`num` both
|
|
90
|
+
rendered it in scientific notation (``9.223372036854776e+18``) *and* lost
|
|
91
|
+
precision, since a float64 cannot represent 2**63-1 exactly. Counters have
|
|
92
|
+
the same problem in miniature — a request total is never fractional.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(value, bool): # bool is an int subclass; not a quantity
|
|
95
|
+
return 0
|
|
96
|
+
if isinstance(value, int):
|
|
97
|
+
return value # already exact — never round-trip through float64
|
|
98
|
+
try:
|
|
99
|
+
return int(float(value))
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
81
104
|
# ── Traefik rule-string parsing ──────────────────────────────────────────────
|
|
82
105
|
# Traefik encodes matching as a rule string: Host(`a.example`) &&
|
|
83
106
|
# PathPrefix(`/api`). We extract hosts and path prefixes for the static
|
|
@@ -17,7 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
from proxy_aiops.ops._util import
|
|
20
|
+
from proxy_aiops.ops._util import (
|
|
21
|
+
as_int,
|
|
22
|
+
as_obj,
|
|
23
|
+
opt,
|
|
24
|
+
parse_rule_hosts,
|
|
25
|
+
parse_rule_paths,
|
|
26
|
+
pick,
|
|
27
|
+
s,
|
|
28
|
+
)
|
|
21
29
|
from proxy_aiops.platform import CADDY, TRAEFIK
|
|
22
30
|
|
|
23
31
|
MAX_ROUTES = 500
|
|
@@ -33,7 +41,7 @@ def _traefik_routes(conn: Any) -> list[dict]:
|
|
|
33
41
|
"platform": TRAEFIK,
|
|
34
42
|
"hosts": [s(h, 128) for h in parse_rule_hosts(rule)],
|
|
35
43
|
"paths": [s(p, 128) for p in parse_rule_paths(rule)],
|
|
36
|
-
"priority":
|
|
44
|
+
"priority": as_int(pick(r, "priority", default=0)),
|
|
37
45
|
"service": opt(pick(r, "service")),
|
|
38
46
|
# Traefik marks a TLS router with a "tls" object that is often
|
|
39
47
|
# EMPTY ({}) — presence, not truthiness, is the signal.
|
|
@@ -99,7 +107,7 @@ def _caddy_routes(conn: Any) -> list[dict]:
|
|
|
99
107
|
"platform": CADDY,
|
|
100
108
|
"hosts": [s(h, 128) for h in hosts],
|
|
101
109
|
"paths": [s(p, 128) for p in paths],
|
|
102
|
-
"priority": 0
|
|
110
|
+
"priority": 0,
|
|
103
111
|
"service": s(_caddy_service(handlers)),
|
|
104
112
|
"tls": tls_ish,
|
|
105
113
|
"enabled": True,
|
|
@@ -118,7 +126,7 @@ def _haproxy_routes(conn: Any) -> list[dict]:
|
|
|
118
126
|
"platform": "haproxy",
|
|
119
127
|
"hosts": [],
|
|
120
128
|
"paths": [],
|
|
121
|
-
"priority": 0
|
|
129
|
+
"priority": 0,
|
|
122
130
|
"service": opt(pick(r, "default_backend", default="")),
|
|
123
131
|
"tls": False,
|
|
124
132
|
"enabled": str(pick(r, "enabled", default="enabled")).lower() != "disabled",
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from proxy_aiops.ops import routes as route_ops
|
|
17
|
-
from proxy_aiops.ops._util import as_obj, num, opt, pick, s
|
|
17
|
+
from proxy_aiops.ops._util import as_int, as_obj, num, opt, pick, s
|
|
18
18
|
from proxy_aiops.platform import CADDY, HAPROXY, TRAEFIK, UnsupportedOperation
|
|
19
19
|
|
|
20
20
|
MAX_ROWS = 500
|
|
@@ -216,14 +216,14 @@ def _haproxy_stat_rows(conn: Any) -> list[dict]:
|
|
|
216
216
|
"status": status,
|
|
217
217
|
"checkStatus": opt(pick(stats, "check_status", default=""), 64),
|
|
218
218
|
"address": opt(pick(stats, "addr", default=""), 200),
|
|
219
|
-
"weight":
|
|
220
|
-
"requestsTotal":
|
|
219
|
+
"weight": as_int(pick(stats, "weight", default=0)),
|
|
220
|
+
"requestsTotal": as_int(pick(stats, "req_tot", default=0)),
|
|
221
221
|
"hrsp2xx": num(pick(stats, "hrsp_2xx", default=0)),
|
|
222
222
|
"hrsp4xx": num(pick(stats, "hrsp_4xx", default=0)),
|
|
223
223
|
"hrsp5xx": num(pick(stats, "hrsp_5xx", default=0)),
|
|
224
|
-
"currentSessions":
|
|
224
|
+
"currentSessions": as_int(pick(stats, "scur", default=0)),
|
|
225
225
|
"rate": num(pick(stats, "rate", default=0)),
|
|
226
|
-
"lastStatusChange":
|
|
226
|
+
"lastStatusChange": as_int(pick(stats, "lastchg", default=0)),
|
|
227
227
|
})
|
|
228
228
|
return out
|
|
229
229
|
|
|
@@ -18,7 +18,7 @@ v0.1 registers three platforms:
|
|
|
18
18
|
read via ``GET /config/``, targeted config writes via ``PATCH/POST/DELETE
|
|
19
19
|
/config/<path>`` and full replace via ``POST /load``. This platform carries
|
|
20
20
|
the write surface (prior-config capture → replayable undo).
|
|
21
|
-
* **haproxy** — HAProxy Data Plane API v2 (``/
|
|
21
|
+
* **haproxy** — HAProxy Data Plane API v3, with v2 fallback (``/v3/services/haproxy/...``,
|
|
22
22
|
HTTP Basic auth): frontends/backends/servers/stats reads plus runtime
|
|
23
23
|
server writes (admin state ready/drain/maint, weight).
|
|
24
24
|
|
|
@@ -288,19 +288,24 @@ _CADDY_UNSUPPORTED = {
|
|
|
288
288
|
),
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
# ─── HAProxy (Data Plane API
|
|
291
|
+
# ─── HAProxy (Data Plane API, HTTP Basic auth) ──────────────────────────────
|
|
292
|
+
# Paths are written for Data Plane API **v3** (HAProxy 3.x). A v2 server is
|
|
293
|
+
# still supported: the connection probes once and rewrites the /v3/ prefix to
|
|
294
|
+
# /v2/ when it sees an older API. Verified live against dataplaneapi v3.0.21,
|
|
295
|
+
# which serves ONLY /v3 — every /v2 path 404s, so hardcoding v2 (as this did)
|
|
296
|
+
# made the whole HAProxy branch unusable on any current HAProxy.
|
|
292
297
|
_HAPROXY_PATHS = {
|
|
293
|
-
"probe": "/
|
|
294
|
-
"version": "/
|
|
295
|
-
"frontends": "/
|
|
296
|
-
"frontend_detail": "/
|
|
297
|
-
"binds": "/
|
|
298
|
-
"backends": "/
|
|
299
|
-
"backend_detail": "/
|
|
300
|
-
"servers": "/
|
|
301
|
-
"runtime_servers": "/
|
|
302
|
-
"runtime_server": "/
|
|
303
|
-
"stats": "/
|
|
298
|
+
"probe": "/v3/info",
|
|
299
|
+
"version": "/v3/info",
|
|
300
|
+
"frontends": "/v3/services/haproxy/configuration/frontends",
|
|
301
|
+
"frontend_detail": "/v3/services/haproxy/configuration/frontends/{name}",
|
|
302
|
+
"binds": "/v3/services/haproxy/configuration/binds?frontend={frontend}",
|
|
303
|
+
"backends": "/v3/services/haproxy/configuration/backends",
|
|
304
|
+
"backend_detail": "/v3/services/haproxy/configuration/backends/{name}",
|
|
305
|
+
"servers": "/v3/services/haproxy/configuration/servers?backend={backend}",
|
|
306
|
+
"runtime_servers": "/v3/services/haproxy/runtime/servers?backend={backend}",
|
|
307
|
+
"runtime_server": "/v3/services/haproxy/runtime/servers/{name}?backend={backend}",
|
|
308
|
+
"stats": "/v3/services/haproxy/stats/native",
|
|
304
309
|
}
|
|
305
310
|
_HAPROXY_UNSUPPORTED = {
|
|
306
311
|
"metrics": (
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proxy-aiops"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "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)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/AIops-tools/Proxy-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "proxy-aiops",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.3.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HAProxy Data Plane API generation must be detected, not assumed.
|
|
2
|
+
|
|
3
|
+
Regression from live verification against HAProxy 3.0 / dataplaneapi v3.0.21,
|
|
4
|
+
which serves ONLY /v3 — every /v2 path 404s. The registry previously hardcoded
|
|
5
|
+
/v2, so the entire HAProxy branch was unusable against any current HAProxy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from unittest.mock import MagicMock
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from proxy_aiops.connection import ProxyConnection
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _conn(probe_status: int) -> ProxyConnection:
|
|
18
|
+
c = ProxyConnection.__new__(ProxyConnection)
|
|
19
|
+
client = MagicMock(name="client")
|
|
20
|
+
client.request.return_value = MagicMock(status_code=probe_status)
|
|
21
|
+
c._client = client
|
|
22
|
+
c._dpapi_prefix = None
|
|
23
|
+
return c
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.unit
|
|
27
|
+
def test_v3_server_keeps_v3_paths():
|
|
28
|
+
c = _conn(200)
|
|
29
|
+
assert c._haproxy_path("/v3/info") == "/v3/info"
|
|
30
|
+
assert c._haproxy_path("/v3/services/haproxy/configuration/backends") == (
|
|
31
|
+
"/v3/services/haproxy/configuration/backends"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.unit
|
|
36
|
+
def test_v2_server_gets_paths_rewritten():
|
|
37
|
+
c = _conn(404) # /v3/info missing => an older Data Plane API
|
|
38
|
+
assert c._haproxy_path("/v3/info") == "/v2/info"
|
|
39
|
+
assert c._haproxy_path("/v3/services/haproxy/configuration/backends") == (
|
|
40
|
+
"/v2/services/haproxy/configuration/backends"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.unit
|
|
45
|
+
def test_probe_is_cached_not_repeated_per_request():
|
|
46
|
+
c = _conn(200)
|
|
47
|
+
for _ in range(5):
|
|
48
|
+
c._haproxy_path("/v3/info")
|
|
49
|
+
assert c._client.request.call_count == 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.unit
|
|
53
|
+
def test_non_versioned_paths_are_untouched():
|
|
54
|
+
c = _conn(200)
|
|
55
|
+
assert c._haproxy_path("/metrics") == "/metrics"
|
|
56
|
+
assert c._client.request.call_count == 0, "must not probe for unrelated paths"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Integer quantities must stay exact integers.
|
|
2
|
+
|
|
3
|
+
Regression from live verification against Traefik 3.2: route priority is an
|
|
4
|
+
int64, and routing it through the float helper both rendered it in scientific
|
|
5
|
+
notation and lost precision — two routers with *different* priorities
|
|
6
|
+
(…806 and …805) both displayed as 9.223372036854776e+18. Route priority decides
|
|
7
|
+
matching order, so collapsing distinct values is actively misleading.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from proxy_aiops.ops._util import as_int, num
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.unit
|
|
18
|
+
def test_as_int_is_exact_for_large_int64_values():
|
|
19
|
+
hi = 9223372036854775806 # 2**63 - 2, as Traefik reports for api@internal
|
|
20
|
+
lo = 9223372036854775805
|
|
21
|
+
assert as_int(hi) == hi
|
|
22
|
+
assert as_int(lo) == lo
|
|
23
|
+
assert as_int(hi) != as_int(lo), "distinct priorities must not collapse"
|
|
24
|
+
# The float helper is exactly what could not do this:
|
|
25
|
+
assert num(hi) == num(lo), "documents why num() is wrong for priorities"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.unit
|
|
29
|
+
def test_as_int_types_and_edges():
|
|
30
|
+
assert isinstance(as_int("42"), int) and as_int("42") == 42
|
|
31
|
+
assert as_int(3.9) == 3
|
|
32
|
+
assert as_int(None) == 0
|
|
33
|
+
assert as_int("nonsense") == 0
|
|
34
|
+
assert as_int(True) == 0, "a bool is not a quantity"
|
|
@@ -44,9 +44,11 @@ def test_path_templates_differ_per_platform():
|
|
|
44
44
|
ha = get_platform(HAPROXY)
|
|
45
45
|
assert tr.path("routers") == "/api/http/routers"
|
|
46
46
|
assert tr.path("router_detail", name="web@file").endswith("/web%40file")
|
|
47
|
-
|
|
47
|
+
# The registry holds Data Plane API **v3** paths (HAProxy 3.x serves only
|
|
48
|
+
# /v3); the connection rewrites them to /v2 for an older server.
|
|
49
|
+
assert ha.path("backends") == "/v3/services/haproxy/configuration/backends"
|
|
48
50
|
assert ha.path("runtime_server", name="web1", backend="app") == (
|
|
49
|
-
"/
|
|
51
|
+
"/v3/services/haproxy/runtime/servers/web1?backend=app"
|
|
50
52
|
)
|
|
51
53
|
assert get_platform(CADDY).path("config_load") == "/load"
|
|
52
54
|
|
|
@@ -1,48 +0,0 @@
|
|
|
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.
|
|
@@ -1,191 +0,0 @@
|
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|