Skip to content
Merged
45 changes: 27 additions & 18 deletions .github/workflows/escript-soundness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,28 +107,37 @@ jobs:
working-directory: source
run: mix deps.get

# NOTE: deliberately NOT --warnings-as-errors, and this is not laziness.
# Turning it on today makes this gate red on arrival, because five real
# warnings sit on main — invisible until now precisely because tests.yml,
# which carried that step, has never had a single successful run.
# --warnings-as-errors is ON, and it is load-bearing.
#
# Two of the five are genuine logic defects, not lint:
# * lib/rules/code_scanning_alerts.ex:544,571 rebind `findings` INSIDE an
# `if` whose value is discarded, so CSA006 builds findings and throws
# them away.
# * lib/rules/secret_scanner_verification.ex:168 rebinds `exit`/`output`
# inside `if exit != 0`, so the trufflehog fallback is inert and the
# following `if exit == 0` still tests the ORIGINAL gitleaks exit.
# Both are the same shape as the CondClauseError this gate exists to catch:
# code that reads as though it acts and does nothing.
# It was deliberately omitted when this gate was first written, because
# 13 real warnings sat on main — invisible until then precisely because
# tests.yml, which carried that step, has never had a single successful
# run. Those 13 are now cleared, so the gate is armed.
#
# Silencing them with an underscore would cement live bugs; repairing them
# changes scanner behaviour across 449 consumers and must be its own
# measured PR. Restore --warnings-as-errors in that PR, not this one.
# Two of them were logic defects rather than lint, and both are the
# shape this gate exists to catch: code that reads as though it acts and
# does nothing.
# * code_scanning_alerts.ex rebound `findings` INSIDE an `if` whose
# value is discarded, so CSA006 built findings and threw them away.
# It ran on all 449 consumers and never produced a finding.
# * secret_scanner_verification.ex was dead in seven independent ways
# at once (string-vs-integer comparison, unguarded System.cmd,
# a 3-tuple destructure of a 2-tuple, the discarded rebinding, and
# writing evidence into the scanned repo). It was referenced
# nowhere, so it was deleted rather than rewritten.
#
# An earlier version of this note said "five warnings" and blamed the
# trufflehog fallback's rebinding for making it inert. Both were wrong:
# the measured count was 13, and that module never reached the fallback
# because `System.cmd(...) |> elem(0) == 0` compares a string to an
# integer and is always false.
#
# If this step goes red, do NOT silence it with an underscore — an
# unused variable here has twice meant a discarded result. Fix the
# logic, or delete the code.
- name: Compile
working-directory: source
run: mix compile

run: mix compile --warnings-as-errors
# The gate itself. Builds the escript fresh and asserts every rule in
# test/soundness/manifest.json fires at its expected severity against the
# built binary — the packaging path, not the in-process one.
Expand Down
24 changes: 9 additions & 15 deletions lib/hypatia/web/api_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,9 @@ defmodule Hypatia.Web.ApiRouter do
end
end

@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 /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 @@ -80,11 +78,9 @@ defmodule Hypatia.Web.ApiRouter do
end
end

@doc """
GET /api/quarantine -- everything currently auto-quarantined:
recipes (verification-rate gate) and bots (consecutive-failure /
FP-rate gate from Hypatia.Safety.Quarantine).
"""
# 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 @@ -102,11 +98,9 @@ defmodule Hypatia.Web.ApiRouter do
})
end

@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 /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
58 changes: 25 additions & 33 deletions lib/hypatia/web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,16 @@ defmodule Hypatia.Web.Router do
plug(:match)
plug(:dispatch)

@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 / -- 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

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

@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 -- 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

@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.
# 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.
"""
#
# 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 @@ -88,14 +81,13 @@ defmodule Hypatia.Web.Router do
# reachable for container orchestrators.
forward("/api", to: Hypatia.Web.ApiRouter)

@doc """
POST /graphql -- GraphQL-shaped query endpoint (M14).
# 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.
"""
#
# 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
conn = Hypatia.Web.ApiRouter.protect(conn, [])

Expand Down
Loading
Loading