Skip to content
Merged
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
150 changes: 73 additions & 77 deletions lib/fleet_dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,54 +45,6 @@ defmodule Hypatia.FleetDispatcher do
end
end

# Standard fleet dispatch path for eliminate tier.
defp dispatch_eliminate_via_fleet(recipe, pattern) do
confidence = Map.get(recipe, "confidence", 0.0)
strategy = TriangleRouter.dispatch_strategy(confidence)

bot_id =
case strategy do
:auto_execute -> "robot-repo-automaton"
:review -> "rhodibot"
:report_only -> "sustainabot"
end

action_type =
case strategy do
:auto_execute -> :commit_push
:review -> :pr_create
:report_only -> :advisory
end

# Gate review -- every action must pass through the Kin Gate
gate_action = %{
bot_id: bot_id,
repo: get_pattern_repo(pattern),
action_type: action_type,
confidence: confidence,
pattern_id: Map.get(pattern, "id", Map.get(pattern, "description", "")),
scan_timestamp: Map.get(pattern, "scan_timestamp"),
dispatch_tier: strategy
}

case gate_review(gate_action) do
{:approved, _} ->
do_eliminate_dispatch(strategy, recipe, pattern, confidence)

{:held, reason} ->
Logger.warning("Gate held eliminate dispatch: #{reason}")
{:ok, :held}

{:rejected, reason} ->
Logger.warning("Gate rejected eliminate dispatch: #{reason}")
{:error, :gate_rejected, reason}

{:deferred, wait_ms} ->
Logger.info("Gate deferred eliminate dispatch -- retry in #{div(wait_ms, 1000)}s")
{:ok, :deferred}
end
end

def dispatch_routed_action({:substitute, recipe, pattern}) do
proven_module = Map.get(recipe, "proven_module", "unknown")

Expand Down Expand Up @@ -123,22 +75,20 @@ defmodule Hypatia.FleetDispatcher do
})
end

@doc """
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)
"""
# 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)
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 @@ -202,19 +152,17 @@ defmodule Hypatia.FleetDispatcher do
end
end

@doc """
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)
"""
# 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)
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 Expand Up @@ -271,6 +219,54 @@ defmodule Hypatia.FleetDispatcher do

# --- Eliminate dispatch helpers (called after Gate approval) ---

# Standard fleet dispatch path for eliminate tier.
defp dispatch_eliminate_via_fleet(recipe, pattern) do
confidence = Map.get(recipe, "confidence", 0.0)
strategy = TriangleRouter.dispatch_strategy(confidence)

bot_id =
case strategy do
:auto_execute -> "robot-repo-automaton"
:review -> "rhodibot"
:report_only -> "sustainabot"
end

action_type =
case strategy do
:auto_execute -> :commit_push
:review -> :pr_create
:report_only -> :advisory
end

# Gate review -- every action must pass through the Kin Gate
gate_action = %{
bot_id: bot_id,
repo: get_pattern_repo(pattern),
action_type: action_type,
confidence: confidence,
pattern_id: Map.get(pattern, "id", Map.get(pattern, "description", "")),
scan_timestamp: Map.get(pattern, "scan_timestamp"),
dispatch_tier: strategy
}

case gate_review(gate_action) do
{:approved, _} ->
do_eliminate_dispatch(strategy, recipe, pattern, confidence)

{:held, reason} ->
Logger.warning("Gate held eliminate dispatch: #{reason}")
{:ok, :held}

{:rejected, reason} ->
Logger.warning("Gate rejected eliminate dispatch: #{reason}")
{:error, :gate_rejected, reason}

{:deferred, wait_ms} ->
Logger.info("Gate deferred eliminate dispatch -- retry in #{div(wait_ms, 1000)}s")
{:ok, :deferred}
end
end

defp do_eliminate_dispatch(:auto_execute, recipe, pattern, confidence) do
recipe_id = Map.get(recipe, "id")

Expand Down
4 changes: 2 additions & 2 deletions lib/hypatia/diagnostics/monitor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ defmodule Hypatia.Diagnostics.Monitor do
nil -> {:error, :neural_unresponsive}
status -> {:ok, {:neural, status}}
end
rescue
_ -> {:error, :neural_crashed}
catch
# Training cycles can take minutes -- a timeout means busy, not crashed
:exit, {:timeout, _} -> {:ok, {:neural, :training_in_progress}}
:exit, _ -> {:error, :neural_crashed}
rescue
_ -> {:error, :neural_crashed}
end
end

Expand Down
78 changes: 34 additions & 44 deletions lib/hypatia/web/api_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,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 @@ -81,11 +79,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 @@ -103,11 +99,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 All @@ -118,20 +112,18 @@ defmodule Hypatia.Web.ApiRouter do
json(conn, 200, %{count: length(rows), rows: rows})
end

@doc """
POST /api/alerts/ingest -- Federation ingress. Peer hypatia
instances POST their alerts here via the Peer sink.

Auth: the auth_gate plug enforces a valid bearer token, so this
endpoint is only reachable when HYPATIA_API_BEARER_TOKEN is set
and the request carries it. Federation without shared auth is
refused at the gate, not here.

Loop prevention: the ingested alert is tagged with
`metadata.federated_from = <peer hostname or "unknown">` so the
Peer sink can skip it on broadcast and the dashboard can
attribute it.
"""
# POST /api/alerts/ingest -- Federation ingress. Peer hypatia
# instances POST their alerts here via the Peer sink.
#
# Auth: the auth_gate plug enforces a valid bearer token, so this
# endpoint is only reachable when HYPATIA_API_BEARER_TOKEN is set
# and the request carries it. Federation without shared auth is
# refused at the gate, not here.
#
# Loop prevention: the ingested alert is tagged with
# `metadata.federated_from = <peer hostname or "unknown">` so the
# Peer sink can skip it on broadcast and the dashboard can
# attribute it.
post "/alerts/ingest" do
{:ok, body, conn} = Plug.Conn.read_body(conn)

Expand Down Expand Up @@ -174,20 +166,18 @@ defmodule Hypatia.Web.ApiRouter do

defp parse_atom(_), do: :unknown

@doc """
GET /api/events -- Server-Sent Events stream of telemetry as it
fires. Each event arrives as

event: hypatia.scan.complete
data: {"measurements": {...}, "metadata": {...}, "at": ms}

Optional `?events=hypatia.scan.complete,hypatia.outcome.recorded`
filter narrows the stream to specific event kinds.

Heartbeats every 15s as comment lines (`: keepalive`) defeat proxy
idle-timeouts. The handler exits cleanly when the client disconnects
(Bandit closes the chunked response).
"""
# GET /api/events -- Server-Sent Events stream of telemetry as it
# fires. Each event arrives as
#
# event: hypatia.scan.complete
# data: {"measurements": {...}, "metadata": {...}, "at": ms}
#
# Optional `?events=hypatia.scan.complete,hypatia.outcome.recorded`
# filter narrows the stream to specific event kinds.
#
# Heartbeats every 15s as comment lines (`: keepalive`) defeat proxy
# idle-timeouts. The handler exits cleanly when the client disconnects
# (Bandit closes the chunked response).
get "/events" do
conn = Plug.Conn.fetch_query_params(conn)
filter = parse_event_filter(conn.query_params["events"])
Expand Down Expand Up @@ -368,7 +358,7 @@ defmodule Hypatia.Web.ApiRouter do
a
|> :binary.bin_to_list()
|> Enum.zip(:binary.bin_to_list(b))
|> Enum.reduce(0, fn {x, y}, acc -> acc ||| Bitwise.bxor(x, y) end) == 0
|> Enum.reduce(0, fn {x, y}, acc -> acc ||| bxor(x, y) end) == 0
end
end

Expand Down
Loading
Loading