Packages

fnord

0.8.85

AI code archaeology

Current section

Files

Jump to
fnord lib services approvals gate.ex
Raw

lib/services/approvals/gate.ex

defmodule Services.Approvals.Gate do
@moduledoc """
Minimal in-memory approvals gate for sensitive "finalize" steps (M4).
Provides a tiny API:
- `require/2` requests approval (`:approved` or `{:pending, ref}`)
- `approve/1` and `deny/2` control a pending reference
- `status/1` and `list/0` allow inspection
Policy:
- Reads `"approvals" -> "mcp_auth_finalize"` from `Settings`.
- Default: `"auto_approve"`. `"require_approval"` returns pending.
Usage:
- Insert a single checkpoint before writing sensitive data (e.g., tokens).
- Return pending + `ref` and instruct operators to use the CLI to approve.
Introduced: M4.
"""
use Agent
@type ref :: String.t()
@type status :: :pending | :approved | {:denied, String.t()}
@spec start_link(keyword()) :: {:ok, pid()} | {:error, term()}
def start_link(_opts \\ []) do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
@doc """
Require approval for a resource. Returns :approved immediately when policy is
auto_approve; otherwise returns {:pending, ref}.
"""
@spec require(resource :: term(), opts :: keyword()) :: :approved | {:pending, ref}
def require(resource, _opts \\ []) do
if auto_approve?(resource) do
:approved
else
ref = gen_ref()
now = System.os_time(:second)
Agent.update(__MODULE__, fn m ->
Map.put(m, ref, %{resource: resource, status: :pending, created_at: now})
end)
{:pending, ref}
end
end
@doc """
Approve a pending reference.
"""
@spec approve(ref) :: :ok | {:error, :not_found}
def approve(ref), do: update(ref, fn m -> %{m | status: :approved} end)
@doc """
Deny a pending reference with a reason.
"""
@spec deny(ref, String.t()) :: :ok | {:error, :not_found}
def deny(ref, reason), do: update(ref, fn m -> %{m | status: {:denied, reason}} end)
@doc """
Get status of a reference.
"""
@spec status(ref) :: status | {:error, :not_found}
def status(ref) do
Agent.get(__MODULE__, fn m ->
case Map.get(m, ref) do
nil -> {:error, :not_found}
%{status: s} -> s
end
end)
end
@doc """
List all current approvals tracked in memory.
"""
@spec list() :: list(map())
def list do
Agent.get(__MODULE__, fn m ->
Enum.map(m, fn {ref, v} -> Map.put(v, :ref, ref) end)
end)
end
# -- internals --
defp update(ref, fun) do
Agent.get_and_update(__MODULE__, fn m ->
case Map.get(m, ref) do
nil -> {{:error, :not_found}, m}
v -> {:ok, Map.put(m, ref, fun.(v))}
end
end)
end
defp gen_ref, do: "appr-" <> Base.url_encode64(:crypto.strong_rand_bytes(8), padding: false)
# Default policy is auto_approve; can be overridden in settings.
# For MVP we only check mcp_auth_finalize policy; everything else auto-approves.
defp auto_approve?({:mcp, _server, :auth_finalize}) do
settings = Settings.new()
approvals = Settings.get(settings, "approvals", %{})
case Map.get(approvals, "mcp_auth_finalize", "auto_approve") do
"require_approval" -> false
_ -> true
end
end
defp auto_approve?(_), do: true
end