defmodule Pi.Eval.Sandbox do @moduledoc """ Restricted Elixir evaluation for untrusted bridge surfaces. This module uses the optional `:dune` dependency when available. It is intended for external/user-entered snippets, not for the trusted project introspection path exposed by `Pi.Eval.run/2`. """ @default_timeout_ms 5_000 @default_max_reductions 50_000 @default_max_heap_size 100_000 @type result :: {:ok, %{value: term(), inspected: String.t(), stdio: String.t()}} | {:error, String.t()} | {:error, :unavailable} @spec available?() :: boolean() def available?, do: Code.ensure_loaded?(Dune) @spec eval(String.t(), keyword()) :: result() def eval(code, opts \\ []) when is_binary(code) do if available?() do do_eval(code, opts) else {:error, :unavailable} end end defp do_eval(code, opts) do timeout = Keyword.get(opts, :timeout, @default_timeout_ms) max_reductions = Keyword.get(opts, :max_reductions, @default_max_reductions) max_heap_size = Keyword.get(opts, :max_heap_size, @default_max_heap_size) allowlist = Keyword.get(opts, :allowlist, configured_allowlist()) dune_opts = [ timeout: timeout, max_reductions: max_reductions, max_heap_size: max_heap_size ] |> maybe_put(:allowlist, allowlist) case Dune.eval_string(code, dune_opts) do %{__struct__: Dune.Success, value: value, inspected: inspected, stdio: stdio} -> {:ok, %{value: value, inspected: inspected, stdio: stdio}} %{__struct__: Dune.Failure, message: message} -> {:error, message} end end defp configured_allowlist do Application.get_env(:pi_bridge, :sandbox_allowlist) end defp maybe_put(opts, _key, nil), do: opts defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value) end