Files
crucible-agent-build-fastap…/docs/service-design.md

3.4 KiB
Raw Blame History

Endpoint Monitor service design

Scope and lifecycle

This repository contains one unauthenticated FastAPI service for defining endpoint monitors and running checks on demand. State is held in a lock-protected, process-local MonitorStore; it starts empty, is lost on restart, and is not shared between workers. Production deployments of this implementation must therefore use exactly one worker. Persistence and authentication are intentionally out of scope.

Resource and routes

A monitor has a server-generated UUID, name, HTTP(S) URL, creation/update timestamps, and a current_status. Status is one of unknown, up, down, or error. unknown has never been checked; up means the final response was 200399; down means it was 400599; error represents a timeout, transport failure, redirect-policy violation, or SSRF rejection.

Method Path Meaning
POST /v1/monitors Create (201)
GET /v1/monitors List, ordered by creation time
GET /v1/monitors/{id} Retrieve
PATCH /v1/monitors/{id} Partially update name/URL
DELETE /v1/monitors/{id} Delete (204)
POST /v1/monitors/{id}/check Run an on-demand check and atomically store it
GET /v1/monitors/{id}/status Retrieve current status
GET /healthz Liveness
GET /readyz Readiness and storage mode

Checks do not download response bodies. Redirects are followed explicitly up to the configured bound. Every hop is parsed, DNS-resolved, policy-checked, and connected through a resolver pinned to the approved addresses. If any answer is loopback, private, link-local, multicast, unspecified, reserved, or otherwise non-global, the hop is rejected. This conservative all-addresses rule and address pinning prevent mixed-answer and DNS-rebinding bypasses. Only HTTP and HTTPS URLs without credentials are accepted.

The checker returns a status representation for expected outbound failures rather than turning remote endpoint behavior into a 5xx response. A concurrent monitor edit causes 409 and prevents a result for the old definition from replacing current state. Missing resources return 404. Validation returns 422. Errors use {"error":{"code":"...","message":"..."}} and never include outbound exception text.

Concurrency and logging

Store operations and compare-and-set status recording are guarded by one asyncio.Lock. Reads return deep copies. A monotonically increasing internal revision makes check updates atomic with respect to edits/deletes.

Application events are one-line JSON records. URLs are normalized for logs by removing user information, fragments, and all query values (?REDACTED). Outbound exception details are classified, not logged verbatim. API resources retain the configured URL because it is part of their explicit contract; operators should still avoid URL credentials and secrets.

Project structure

  • app/main.py: application factory, lifespan, handlers
  • app/api.py: REST contract
  • app/models.py: typed request/response models
  • app/store.py: concurrency-safe process-local state
  • app/checker.py: SSRF-safe checker and redacted event logging
  • app/settings.py: validated MONITOR_ environment configuration
  • tests/: unit and API tests with no real outbound traffic
  • Dockerfile, compose.yaml, pyproject.toml: packaging and workflows
  • docs/verification.md: validation checklist and observed limitations