defmodule ASM.Permission do @moduledoc """ Normalizes permission modes into a common runtime model. Normalized set: - `:default` - `:auto` - `:bypass` - `:plan` """ alias ASM.Error @type provider :: :amp | :antigravity | :claude | :codex | :codex_exec | :cursor | :shell | atom() @type normalized_mode :: :default | :auto | :bypass | :plan @type native_mode :: atom() @type normalization :: %{normalized: normalized_mode(), native: native_mode()} @normalized_modes [:default, :auto, :bypass, :plan] @string_modes %{ "default" => :default, "auto" => :auto, "bypass" => :bypass, "plan" => :plan, "accept_edits" => :accept_edits, "bypass_permissions" => :bypass_permissions, "delegate" => :delegate, "dont_ask" => :dont_ask, "auto_edit" => :auto_edit, "yolo" => :yolo, "force" => :bypass, "dangerously_allow_all" => :dangerously_allow_all, "dangerously_skip_permissions" => :dangerously_skip_permissions } @spec normalized_modes() :: [normalized_mode()] def normalized_modes, do: @normalized_modes @spec valid_modes(provider()) :: [atom()] def valid_modes(provider) do provider |> canonical_provider() |> provider_mode_map() |> Map.keys() end @spec normalize(provider(), atom() | String.t()) :: {:ok, normalization()} | {:error, Error.t()} def normalize(provider, mode) do provider = canonical_provider(provider) with {:ok, mode_atom} <- normalize_mode_atom(mode) do normalize_for_provider(provider, mode_atom) end end @spec normalize!(provider(), atom() | String.t()) :: normalization() def normalize!(provider, mode) do case normalize(provider, mode) do {:ok, normalized} -> normalized {:error, %Error{} = error} -> raise ArgumentError, Exception.message(error) end end @spec canonical_provider(provider()) :: provider() def canonical_provider(:codex), do: :codex_exec def canonical_provider(provider), do: provider defp normalize_mode_atom(mode) when is_atom(mode), do: {:ok, mode} defp normalize_mode_atom(mode) when is_binary(mode) do normalized = mode |> String.trim() |> String.downcase() |> then(&Map.get(@string_modes, &1)) if normalized do {:ok, normalized} else {:error, config_error("Unsupported permission mode: #{inspect(mode)}")} end end defp normalize_mode_atom(other) do {:error, config_error("Invalid permission mode type: #{inspect(other)}")} end defp normalize_for_provider(provider, mode_atom) do case Map.fetch(provider_mode_map(provider), mode_atom) do {:ok, normalized} -> {:ok, normalized} :error -> {:error, config_error( "Permission mode #{inspect(mode_atom)} is not valid for provider #{inspect(provider)}", provider: provider, mode: mode_atom )} end end defp provider_mode_map(:claude) do %{ default: %{normalized: :default, native: :default}, auto: %{normalized: :auto, native: :accept_edits}, accept_edits: %{normalized: :auto, native: :accept_edits}, delegate: %{normalized: :auto, native: :delegate}, dont_ask: %{normalized: :auto, native: :dont_ask}, bypass: %{normalized: :bypass, native: :bypass_permissions}, bypass_permissions: %{normalized: :bypass, native: :bypass_permissions}, plan: %{normalized: :plan, native: :plan} } end defp provider_mode_map(:codex_exec) do %{ default: %{normalized: :default, native: :default}, bypass: %{normalized: :bypass, native: :yolo}, yolo: %{normalized: :bypass, native: :yolo}, plan: %{normalized: :plan, native: :plan} } end defp provider_mode_map(:amp) do %{ default: %{normalized: :default, native: :default}, auto: %{normalized: :auto, native: :auto}, bypass: %{normalized: :bypass, native: :dangerously_allow_all}, dangerously_allow_all: %{normalized: :bypass, native: :dangerously_allow_all}, plan: %{normalized: :plan, native: :plan} } end defp provider_mode_map(:antigravity) do %{ default: %{normalized: :default, native: :default}, bypass: %{normalized: :bypass, native: :bypass}, dangerously_skip_permissions: %{normalized: :bypass, native: :bypass} } end defp provider_mode_map(:cursor) do %{ default: %{normalized: :default, native: :default}, bypass: %{normalized: :bypass, native: :bypass}, plan: %{normalized: :plan, native: :plan} } end defp provider_mode_map(:shell) do %{ default: %{normalized: :default, native: :default}, auto: %{normalized: :auto, native: :auto}, bypass: %{normalized: :bypass, native: :bypass}, plan: %{normalized: :plan, native: :plan} } end defp provider_mode_map(_other) do %{ default: %{normalized: :default, native: :default}, auto: %{normalized: :auto, native: :auto}, bypass: %{normalized: :bypass, native: :bypass}, plan: %{normalized: :plan, native: :plan} } end defp config_error(message, details \\ %{}) do Error.new(:config_invalid, :config, message, cause: details) end end