Files
crucible-agent-build-fastap…/docs/SERVICE_DESIGN.md

2.8 KiB
Raw Blame History

Service contract and architecture

Resource

A monitor has an immutable UUID id, name, HTTP(S) url, timestamps, and current status. status is unknown until checked, up for a final HTTP 200399 response, down for a final HTTP 400599 response, and error when no accepted HTTP response is obtained. Check metadata comprises checked_at, non-negative latency_ms, optional http_status, and a bounded, non-sensitive error category.

API

All JSON is UTF-8. Routes are under /api/v1 except operational probes.

  • POST /api/v1/monitors -> 201 and monitor; duplicate names are allowed.
  • GET /api/v1/monitors -> 200 list sorted by creation time and id.
  • GET /api/v1/monitors/{id} -> 200 or 404.
  • PUT /api/v1/monitors/{id} -> 200 or 404. Updating name/URL resets check state to unknown.
  • DELETE /api/v1/monitors/{id} -> 204 or 404.
  • POST /api/v1/monitors/{id}/check -> 200 and atomically persisted check result, or 404 if absent/deleted during the check.
  • GET /api/v1/monitors/{id}/status -> 200 current status projection or 404.
  • GET /healthz -> liveness 200; GET /readyz -> readiness 200 after app initialization.

FastAPI's validation errors use 422. Application errors use {"detail":"..."} and do not expose network exception text. There is intentionally no authentication.

Checker and SSRF policy

Only HTTP(S), non-credentialed URLs are accepted. Before every request and after every redirect, the host is resolved and every returned address must be globally routable. Loopback, private, link-local, multicast, reserved, unspecified, and non-global addresses are rejected. Redirects are followed manually up to the configured bound. Timeouts and response-body streaming are bounded. This validation substantially reduces SSRF risk, but DNS validation and connection are separate operations in the standard HTTP stack; production deployments should additionally enforce an egress proxy/firewall to eliminate DNS-rebinding TOCTOU risk.

Checks use a monotonic timer. Store mutation occurs under one asyncio lock. A delete that races with a check wins and the result is not resurrected.

Lifecycle and deployment

MonitorStore is process-local and starts empty on every process start. It is concurrency-safe within one event loop, not shared across workers, and has no persistence. Run exactly one worker. Readiness describes initialization, not durability or external dependency health.

Configuration is environment-driven and validated at startup. Logs are one-line JSON. Logged URLs have credentials, query, and fragment removed; only controlled error categories are logged.

Project layout

app/models.py defines the contract; store.py owns state; security.py validates targets; checker.py performs checks; main.py wires HTTP routes and lifecycle. Tests inject DNS and HTTP transports so outbound calls are deterministic.