Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 17 additions & 25 deletions lib/fleet_dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ defmodule Hypatia.FleetDispatcher do
- {:eliminate, recipe, pattern} -- auto-fix or PR depending on confidence
- {:substitute, recipe, pattern} -- PR with proven module + proof obligation
- {:control, pattern} -- advisory report to sustainabot

Proof obligation recipes are produced by
`ProofObligation.obligations_from_patterns/2` and use triangle routing:
- `:eliminate` (auto-provable, confidence >= 0.90) applies a tactic inline
- `:eliminate` (confidence < 0.90) goes to echidnabot with an eliminate-tier hint
- `:substitute` goes to echidnabot with a VeriSimDB-recommended prover hint
- `:control` creates a sustainabot advisory when human review is required

Dependabot alert recipes are produced by `DependabotAlerts.fixes_from_alerts/3`
and use triangle routing:
- `:eliminate` + confidence >= 0.95 auto-bumps through robot-repo-automaton
(subject to Kin Gate, rate limiter, and exclusion registry)
- `:eliminate` + confidence in [0.85, 0.95) opens a rhodibot PR
- `:substitute` opens a rhodibot PR for a major or breaking bump
- `:control` creates a sustainabot advisory when no auto-fix path is available
"""
def dispatch_routed_action({:eliminate, recipe, pattern}) do
# Direct PR path: when HYPATIA_DIRECT_PR=true and the finding is a
Expand Down Expand Up @@ -75,20 +90,7 @@ defmodule Hypatia.FleetDispatcher do
})
end

# Dispatch a ProofObligation recipe through the Safety Triangle.
#
# Called by `ProofObligation.obligations_from_patterns/2` and any code
# that constructs `{:proof_obligation, recipe, pattern}` tuples.
#
# Triangle routing for proof obligations:
# - `:eliminate` (auto-provable, confidence >= 0.90) ->
# robot-repo-automaton applies tactic inline
# - `:eliminate` (confidence < 0.90) ->
# echidnabot with eliminate-tier hint
# - `:substitute` ->
# echidnabot with VeriSimDB-recommended prover hint
# - `:control` ->
# sustainabot advisory (sorry/Admitted present, human required)
# ProofObligation recipes include optional prover and tactic hints.
def dispatch_routed_action({:proof_obligation, recipe, pattern}) do
tier = Map.get(recipe, "triangle_tier", "substitute")
claim = Map.get(recipe, "claim", Map.get(pattern, "description", ""))
Expand Down Expand Up @@ -152,17 +154,7 @@ defmodule Hypatia.FleetDispatcher do
end
end

# Dispatch a DependabotAlerts recipe through the Safety Triangle.
#
# Called by `DependabotAlerts.fixes_from_alerts/3` and any code that
# constructs `{:dependabot_fix, recipe, pattern}` tuples.
#
# Triangle routing for Dependabot alerts:
# - `:eliminate` + confidence >= 0.95 -> robot-repo-automaton auto-bumps
# (subject to Kin Gate, rate limiter, exclusion registry)
# - `:eliminate` + confidence in [0.85, 0.95) -> rhodibot opens a PR
# - `:substitute` -> rhodibot opens a PR (major bump / breaking change)
# - `:control` -> sustainabot advisory (no auto-fix path)
# Dependabot recipes use the standard eliminate dispatch and its safeguards.
def dispatch_routed_action({:dependabot_fix, recipe, pattern}) do
tier = Map.get(recipe, "triangle_tier", "control")
confidence = Map.get(recipe, "confidence", 0.5)
Expand Down
24 changes: 15 additions & 9 deletions lib/hypatia/web/api_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ defmodule Hypatia.Web.ApiRouter do
end
end

# GET /api/recipes/:id -- single-recipe drill-down. Returns the same
# shape as one row from `/api/recipes`, plus the recipe definition
# itself when found in the registry.
@doc """
GET /api/recipes/:id -- single-recipe drill-down. Returns the same
shape as one row from `/api/recipes`, plus the recipe definition
itself when found in the registry.
"""
get "/recipes/:id" do
health = Hypatia.OutcomeTracker.recipe_health()
row = Enum.find(health, &(&1.recipe_id == id))
Expand All @@ -79,9 +81,11 @@ defmodule Hypatia.Web.ApiRouter do
end
end

# GET /api/quarantine -- everything currently auto-quarantined:
# recipes (verification-rate gate) and bots (consecutive-failure /
# FP-rate gate from Hypatia.Safety.Quarantine).
@doc """
GET /api/quarantine -- everything currently auto-quarantined:
recipes (verification-rate gate) and bots (consecutive-failure /
FP-rate gate from Hypatia.Safety.Quarantine).
"""
get "/quarantine" do
recipes =
Hypatia.OutcomeTracker.recipe_health()
Expand All @@ -99,9 +103,11 @@ defmodule Hypatia.Web.ApiRouter do
})
end

# GET /api/alerts -- Recent threshold-rule alerts emitted by
# Hypatia.Watcher.Alerts (ring buffer, newest first). Powers the
# dashboard alert ribbon and supports manual triage.
@doc """
GET /api/alerts -- Recent threshold-rule alerts emitted by
Hypatia.Watcher.Alerts (ring buffer, newest first). Powers the
dashboard alert ribbon and supports manual triage.
"""
get "/alerts" do
rows =
case Process.whereis(Hypatia.Watcher.Alerts) do
Expand Down
60 changes: 35 additions & 25 deletions lib/hypatia/web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ defmodule Hypatia.Web.Router do
plug(:match)
plug(:dispatch)

# GET / -- Single-page live operational dashboard. HTML + vanilla JS,
# polls /api/status and EventSource-streams /api/events. The dashboard
# itself is publicly reachable; the data endpoints it calls are
# loopback-only (gated in ApiRouter), so a non-local browser would
# render the chrome but get 403 from the XHR/SSE calls.
@doc """
GET / -- Single-page live operational dashboard. HTML + vanilla JS,
polls /api/status and EventSource-streams /api/events. The dashboard
itself is publicly reachable; the data endpoints it calls are
loopback-only (gated in ApiRouter), so a non-local browser would
render the chrome but get 403 from the XHR/SSE calls.
"""
get "/" do
Hypatia.Web.Dashboard.call(conn, [])
end

# GET /health -- Basic health check for the HTTP endpoint.
@doc """
GET /health -- Basic health check for the HTTP endpoint.
"""
get "/health" do
health = %{
status: "ok",
Expand All @@ -50,23 +54,27 @@ defmodule Hypatia.Web.Router do
|> send_resp(200, Jason.encode!(health))
end

# GET /metrics -- Prometheus text-format exposition. Publicly
# reachable (NOT loopback-only) because scrapers routinely run on a
# different host; there's no operational data in the metric body
# that isn't already implied by the dashboard's existence.
@doc """
GET /metrics -- Prometheus text-format exposition. Publicly
reachable (NOT loopback-only) because scrapers routinely run on a
different host; there's no operational data in the metric body
that isn't already implied by the dashboard's existence.
"""
get "/metrics" do
Hypatia.Web.Metrics.call(conn, [])
end

# GET /metrics/snapshot -- Compact JSON snapshot of estate-level
# counters: repos scanned, weak points, dispatched actions, outcomes,
# recipes, average confidence. Consumed by the optional Ada TUI
# (`lib/tui/port.ex`) on its 10s tick, and useful as a single-call
# status read for external dashboards.
#
# Reads from the verisim-data flat-file store via VerisimConnector;
# any failure returns a degraded snapshot with status="degraded"
# rather than 500, so the TUI keeps rendering.
@doc """
GET /metrics/snapshot -- Compact JSON snapshot of estate-level
counters: repos scanned, weak points, dispatched actions, outcomes,
recipes, average confidence. Consumed by the optional Ada TUI
(`lib/tui/port.ex`) on its 10s tick, and useful as a single-call
status read for external dashboards.

Reads from the verisim-data flat-file store via VerisimConnector;
any failure returns a degraded snapshot with status="degraded"
rather than 500, so the TUI keeps rendering.
"""
get "/metrics/snapshot" do
snapshot = Hypatia.Web.MetricsSnapshot.build()

Expand All @@ -80,12 +88,14 @@ defmodule Hypatia.Web.Router do
# reachable for container orchestrators.
forward("/api", to: Hypatia.Web.ApiRouter)

# POST /graphql -- GraphQL-shaped query endpoint (M14).
#
# Minimal hand-rolled implementation; no introspection, no schema
# federation, no Absinthe dep. See lib/hypatia/web/graphql.ex for
# the supported field set and limitations. Loopback-only by sharing
# the bearer-auth gate when HYPATIA_API_BEARER_TOKEN is configured.
@doc """
POST /graphql -- GraphQL-shaped query endpoint (M14).

Minimal hand-rolled implementation; no introspection, no schema
federation, no Absinthe dep. See lib/hypatia/web/graphql.ex for
the supported field set and limitations. Loopback-only by sharing
the bearer-auth gate when HYPATIA_API_BEARER_TOKEN is configured.
"""
post "/graphql" do
Hypatia.Web.GraphQL.call(conn, [])
end
Expand Down
Loading