feat(serve): add authenticated /statusz operational endpoint

Machine-readable JSON server state on the MCP HTTP transport: version,
uptime, request counters by status, SQLite integrity, writer mode, and
recently observed clients (peer, user-agent, last path). Follows the
bearer-token policy since it exposes operational metadata; /healthz
remains the unauthenticated liveness probe.

(cherry picked from commit 0fb07f2645c0c853337419175f499daab8063888)
This commit is contained in:
Igor Lins e Silva 2026-07-02 00:23:40 -03:00
parent 3eee0d7e49
commit 3bed756b6a
3 changed files with 209 additions and 30 deletions

View File

@ -55,6 +55,7 @@ import time # noqa: E402
from datetime import date, datetime # noqa: E402
from pathlib import Path # noqa: E402
from typing import Optional # noqa: E402
from urllib.parse import urlparse # noqa: E402
from .config import ( # noqa: E402
MempalaceConfig,
@ -5311,6 +5312,8 @@ def _json_rpc_parse_error(req_id=None):
# can reference them as free names without a NameError.
_HTTP_REQUEST_LOCK = threading.Lock()
_HTTP_MAX_REQUEST_BYTES = 16 * 1024 * 1024
_HTTP_ACTIVE_CLIENT_WINDOW_S = 120.0
_HTTP_RECENT_CLIENT_LIMIT = 50
# Host literals that always denote this machine. Used both to decide whether a
# bind is loopback (skip the network-exposure warning) and to pin the Host
# header against DNS rebinding when serving on loopback.
@ -5407,6 +5410,143 @@ def _http_origin_allowed(origin: str) -> bool:
return host in ("127.0.0.1", "localhost", "::1")
def _http_client_identity(handler) -> tuple[str, dict]:
headers = handler.headers
peer = handler.client_address[0] if handler.client_address else ""
forwarded_for = (headers.get("X-Forwarded-For") or "").split(",", 1)[0].strip()
real_ip = (headers.get("X-Real-IP") or "").strip()
tailnet_user = (headers.get("Tailscale-User-Login") or "").strip()
user_agent = (headers.get("User-Agent") or "").strip()
host_hdr = (headers.get("Host") or "").strip()
peer_hint = forwarded_for or real_ip or peer
basis = "|".join([peer_hint, tailnet_user, user_agent, host_hdr])
client_id = hashlib.sha256(basis.encode("utf-8", "replace")).hexdigest()[:16]
return client_id, {
"client_id": client_id,
"peer": peer,
"peer_hint": peer_hint,
"host": host_hdr,
"user_agent": user_agent[:160],
"tailscale_user": tailnet_user[:160],
}
def _http_record_request(httpd, handler, status: int) -> None:
now = time.time()
now_iso = datetime.now().isoformat()
client_id, identity = _http_client_identity(handler)
with httpd.stats_lock:
httpd.request_count += 1
httpd.status_counts[str(status)] = httpd.status_counts.get(str(status), 0) + 1
entry = dict(httpd.recent_clients.get(client_id, identity))
entry.update(identity)
entry["last_seen"] = now_iso
entry["last_seen_monotonic"] = now
entry["request_count"] = int(entry.get("request_count", 0)) + 1
entry["last_method"] = handler.command
entry["last_path"] = urlparse(handler.path).path
entry["last_status"] = status
entry["authenticated"] = bool(
httpd.auth_token
and hmac.compare_digest(
handler.headers.get("Authorization", ""), f"Bearer {httpd.auth_token}"
)
)
httpd.recent_clients[client_id] = entry
overflow = len(httpd.recent_clients) - _HTTP_RECENT_CLIENT_LIMIT
if overflow > 0:
oldest = sorted(
httpd.recent_clients.items(),
key=lambda item: item[1].get("last_seen_monotonic", 0.0),
)
for stale_id, _entry in oldest[:overflow]:
httpd.recent_clients.pop(stale_id, None)
def _http_status_payload(httpd) -> dict:
now = time.time()
with httpd.stats_lock:
recent = sorted(
(dict(entry) for entry in httpd.recent_clients.values()),
key=lambda entry: entry.get("last_seen_monotonic", 0.0),
reverse=True,
)
request_count = httpd.request_count
status_counts = dict(httpd.status_counts)
active = []
for entry in recent:
last_seen_monotonic = entry.pop("last_seen_monotonic", 0.0)
if now - last_seen_monotonic <= _HTTP_ACTIVE_CLIENT_WINDOW_S:
active.append(dict(entry))
writer = {
"read_only": _READ_ONLY,
"peer_writer_read_only": _MCP_WRITER_READ_ONLY,
"peer_writer_lock_failed": _MCP_WRITER_LOCK_FAILED,
}
if _MCP_WRITER_LOCK_ERROR:
writer["peer_writer_lock_error"] = _MCP_WRITER_LOCK_ERROR
integrity = _sqlite_integrity_payload()
palace_path = (
os.path.abspath(os.path.expanduser(_config.palace_path)) if _config.palace_path else ""
)
return {
"ok": bool(integrity.get("ok")),
"server": {
"name": "mempalace",
"version": __version__,
"transport": "http",
"scheme": getattr(httpd, "scheme", "http"),
"bind_host": httpd.bind_host,
"port": httpd.server_address[1],
"started_at": httpd.started_at,
"uptime_seconds": round(time.monotonic() - httpd.started_monotonic, 3),
},
"palace": {
"path_hash": hashlib.sha256(palace_path.encode("utf-8")).hexdigest()[:16]
if _config.palace_path
else "",
"backend": _config.backend,
"sqlite_integrity": integrity,
},
"writer": writer,
"requests": {
"total": request_count,
"by_status": status_counts,
},
"clients": {
"active_window_seconds": _HTTP_ACTIVE_CLIENT_WINDOW_S,
"active": active,
"recent": recent,
},
}
def _http_request_rejected(handler, require_auth: bool) -> bool:
"""Enforce HTTP Host/Origin/auth policy before dispatching a request."""
srv = handler.server
if srv.enforce_host_pin:
host_hdr = (handler.headers.get("Host") or "").strip().lower()
if host_hdr not in srv.allowed_hosts:
logger.warning("HTTP request rejected: Host %r not allowed", host_hdr)
handler.send_error(403, "Forbidden")
return True
origin = handler.headers.get("Origin")
if origin and not _http_origin_allowed(origin):
logger.warning("HTTP request rejected: cross-origin %r", origin)
handler.send_error(403, "Forbidden")
return True
if require_auth and srv.auth_token:
provided = handler.headers.get("Authorization", "")
if not hmac.compare_digest(provided, f"Bearer {srv.auth_token}"):
logger.warning("HTTP request rejected: missing/invalid bearer token")
handler.send_error(401, "Unauthorized")
return True
return False
def _build_http_server(host: str, port: int):
"""Construct (but do not start) the MCP HTTP server.
@ -5417,7 +5557,6 @@ def _build_http_server(host: str, port: int):
allowlist, Origin check, optional bearer token) is attached as attributes.
"""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
auth_token = os.environ.get("MEMPALACE_MCP_HTTP_TOKEN", "").strip()
if (
@ -5469,7 +5608,18 @@ def _build_http_server(host: str, port: int):
def log_message(self, fmt, *args):
logger.info("HTTP %s - " + fmt, self.client_address[0], *args)
def send_error(self, code, message=None, explain=None):
self._record_request(code)
return super().send_error(code, message, explain)
def _record_request(self, status: int) -> None:
_http_record_request(self.server, self, status)
def _status_payload(self) -> dict:
return _http_status_payload(self.server)
def _send_bytes(self, status: int, body: bytes, content_type: str) -> None:
self._record_request(status)
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
@ -5483,42 +5633,24 @@ def _build_http_server(host: str, port: int):
self._send_bytes(status, body, "application/json; charset=utf-8")
def _request_rejected(self, require_auth: bool) -> bool:
"""Enforce the transport's access policy before any dispatch.
The palace is the most sensitive data MemPalace holds and ``/mcp``
is unauthenticated by default, so this guards the two ways a local
HTTP server leaks to the network: DNS rebinding (Host/Origin) and,
when the operator opts in, a missing/incorrect bearer token.
"""
srv = self.server
if srv.enforce_host_pin:
host_hdr = (self.headers.get("Host") or "").strip().lower()
if host_hdr not in srv.allowed_hosts:
logger.warning("HTTP request rejected: Host %r not allowed", host_hdr)
self.send_error(403, "Forbidden")
return True
origin = self.headers.get("Origin")
if origin and not _http_origin_allowed(origin):
logger.warning("HTTP request rejected: cross-origin %r", origin)
self.send_error(403, "Forbidden")
return True
if require_auth and srv.auth_token:
provided = self.headers.get("Authorization", "")
if not hmac.compare_digest(provided, f"Bearer {srv.auth_token}"):
logger.warning("HTTP request rejected: missing/invalid bearer token")
self.send_error(401, "Unauthorized")
return True
return False
return _http_request_rejected(self, require_auth)
def do_GET(self):
# Liveness probe is policy-gated for Host/Origin but never requires
# the token, so an orchestrator's health check works without creds.
if self._request_rejected(require_auth=False):
return
path = urlparse(self.path).path
if path == "/healthz":
if self._request_rejected(require_auth=False):
return
self._send_bytes(200, b"ok\n", "text/plain; charset=utf-8")
return
if path == "/statusz":
if self._request_rejected(require_auth=True):
return
self._send_json(200, self._status_payload())
return
if self._request_rejected(require_auth=False):
return
self.send_error(404, "Not Found")
@ -5562,6 +5694,7 @@ def _build_http_server(host: str, port: int):
if response is None:
# JSON-RPC notifications intentionally have no response body.
self._record_request(202)
self.send_response(202)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
@ -5581,6 +5714,13 @@ def _build_http_server(host: str, port: int):
httpd.allowed_hosts = _http_allowed_host_values(host, bound_port)
httpd.auth_token = auth_token
httpd.scheme = "http"
httpd.bind_host = host
httpd.started_at = datetime.now().isoformat()
httpd.started_monotonic = time.monotonic()
httpd.stats_lock = threading.Lock()
httpd.request_count = 0
httpd.status_counts = {}
httpd.recent_clients = {}
if tls_cert:
httpd.socket = _wrap_tls(httpd.socket, tls_cert, tls_key)
httpd.scheme = "https"

View File

@ -106,6 +106,33 @@ def test_healthz_ok(http_server):
assert body == b"ok\n"
def test_statusz_reports_machine_readable_server_and_client_state(http_server, monkeypatch):
monkeypatch.setattr(mcp, "_sqlite_integrity_payload", lambda: {"ok": True, "errors": []})
port, _ = http_server
assert _get(port, "/healthz", headers={"User-Agent": "codex-test"})[0] == 200
status, body = _get(port, "/statusz", headers={"User-Agent": "codex-test"})
assert status == 200
payload = json.loads(body)
assert payload["ok"] is True
assert payload["server"]["name"] == "mempalace"
assert payload["server"]["transport"] == "http"
assert payload["server"]["port"] == port
assert payload["requests"]["total"] >= 1
assert payload["requests"]["by_status"]["200"] >= 1
assert payload["clients"]["active_window_seconds"] == mcp._HTTP_ACTIVE_CLIENT_WINDOW_S
assert payload["clients"]["recent"]
first = payload["clients"]["recent"][0]
assert first["peer"] == "127.0.0.1"
assert first["peer_hint"] == "127.0.0.1"
assert first["user_agent"] == "codex-test"
assert first["last_path"] == "/healthz"
assert "Authorization" not in json.dumps(payload)
if mcp._config.palace_path:
assert mcp._config.palace_path not in json.dumps(payload)
def test_unknown_path_404(http_server):
port, _ = http_server
assert _post(port, "/nope", {"jsonrpc": "2.0", "id": 1, "method": "ping"})[0] == 404
@ -179,6 +206,7 @@ def test_allows_loopback_origin(http_server):
def test_bearer_token_enforced_when_configured(monkeypatch):
"""With MEMPALACE_MCP_HTTP_TOKEN set, /mcp requires a matching bearer token."""
monkeypatch.setenv("MEMPALACE_MCP_HTTP_TOKEN", "s3cret")
monkeypatch.setattr(mcp, "_sqlite_integrity_payload", lambda: {"ok": True, "errors": []})
httpd = mcp._build_http_server("127.0.0.1", 0)
port = httpd.server_address[1]
thread = threading.Thread(
@ -195,6 +223,11 @@ def test_bearer_token_enforced_when_configured(monkeypatch):
assert _post(port, "/mcp", ping, headers={"Authorization": "Bearer s3cret"})[0] == 200
# /healthz never requires the token (orchestrator liveness probes).
assert _get(port, "/healthz")[0] == 200
# /statusz exposes server/client metadata, so it follows the auth policy.
assert _get(port, "/statusz")[0] == 401
status, body = _get(port, "/statusz", headers={"Authorization": "Bearer s3cret"})
assert status == 200
assert "recent" in json.loads(body)["clients"]
finally:
httpd.shutdown()
httpd.server_close()

View File

@ -169,6 +169,8 @@ Other MCP clients use the same two ingredients — the `…/mcp` URL and an
```bash
curl https://memory.example.com/healthz # -> ok
curl https://memory.example.com/statusz \
-H "Authorization: Bearer $MEMPALACE_MCP_HTTP_TOKEN"
```
Once connected, all of MemPalace's [MCP tools](/guide/mcp-integration) operate
@ -189,7 +191,11 @@ whole team.
process safely handles concurrent reads and writes. Don't point two server
processes at the same backend collection.
- **Health checks**: `GET /healthz` returns `200 ok` without a token, so it
works as a load-balancer/Kubernetes liveness probe.
works as a load-balancer/Kubernetes liveness probe. For machine-readable
server state, `GET /statusz` returns JSON with version, uptime, request
counters, SQLite integrity, writer mode, and recent observed MCP clients.
`/statusz` follows the bearer-token policy because it exposes operational
metadata; it is not a public liveness probe.
- **Fronting proxies (Tailscale, nginx)**: the recommended personal-fleet
setup is a loopback bind behind a tailnet-only proxy — nothing touches the
physical LAN and the tailnet provides encryption plus device identity: