| defmodule PerplexityMacro.ToolRouter do
|
| @moduledoc "Capability Router — VM owns control, host owns evidence. Sensitive caps require CONFIRM 0x2A."
|
| use GenServer
|
| @confirm_ttl_ms 60_000
|
| def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
| @impl true
|
| def init(_opts), do: {:ok, %{confirm: nil}}
|
|
|
| @doc "Route RQ → either WASM Box (python) or ResearchBroker (external). Policy: sensitive needs CONFIRM."
|
| def route(req), do: GenServer.call(__MODULE__, {:route, req})
|
| def confirm, do: GenServer.call(__MODULE__, :confirm)
|
|
|
| @impl true
|
| def handle_call({:route, %{capability: cap, python: true}=req}, _from, state) do
|
| if PerplexityMacro.Capability.sensitive?(cap) and not valid_confirm?(state.confirm) do
|
| {:reply, {:error, :confirm_required}, state}
|
| else
|
|
|
| result = PerplexityMacro.WASMBox.execute(req.code, req.args)
|
| tlv = PerplexityMacro.Evidence.encode_tlv(result)
|
| {:reply, {:ok, tlv}, maybe_consume_confirm(state, cap)}
|
| end
|
| end
|
| def handle_call({:route, %{capability: cap}=req}, _from, state) do
|
| if PerplexityMacro.Capability.sensitive?(cap) and not valid_confirm?(state.confirm) do
|
| {:reply, {:error, :confirm_required}, state}
|
| else
|
| result = dispatch_external(req)
|
| tlv = PerplexityMacro.Evidence.encode_tlv(result)
|
| {:reply, {:ok, tlv}, maybe_consume_confirm(state, cap)}
|
| end
|
| end
|
| def handle_call(:confirm, _from, state) do
|
| tok = %{token: :crypto.strong_rand_bytes(4) |> Base.encode16(), exp: System.monotonic_time(:millisecond) + @confirm_ttl_ms}
|
| {:reply, {:ok, tok}, %{state | confirm: tok}}
|
| end
|
|
|
| defp valid_confirm?(nil), do: false
|
| defp valid_confirm?(%{exp: exp}), do: System.monotonic_time(:millisecond) < exp
|
| defp maybe_consume_confirm(state, cap) do
|
| if PerplexityMacro.Capability.sensitive?(cap), do: %{state | confirm: nil}, else: state
|
| end
|
| defp dispatch_external(%{capability: :search}=r), do: PerplexityMacro.Adapters.Search.run(r.query)
|
| defp dispatch_external(%{capability: :wikipedia}=r), do: PerplexityMacro.Adapters.Wikipedia.run(r.query)
|
| defp dispatch_external(%{capability: :mathematica}=r), do: PerplexityMacro.Adapters.Mathematica.run(r.query)
|
| defp dispatch_external(%{capability: :dictionary}=r), do: PerplexityMacro.Adapters.Dictionary.run(r.query)
|
| defp dispatch_external(%{capability: :fetch}=r), do: PerplexityMacro.Adapters.Fetch.run(r.query)
|
| defp dispatch_external(%{capability: :browser}=r), do: PerplexityMacro.Adapters.Browser.run(r.query)
|
| defp dispatch_external(%{capability: :code}=r), do: PerplexityMacro.Adapters.Code.run(r.query)
|
| defp dispatch_external(%{capability: :local_model}=r), do: PerplexityMacro.Adapters.LocalModel.run(r.query)
|
| defp dispatch_external(r), do: {:ok, [%{title: "#{r.capability}", url: "#{r.capability}://", content: inspect(r), score: 1.0, source_hash: :erlang.phash2(r, 0xFFFFFFFF)}]}
|
| end
|
|
|