Current section

Files

Jump to
agent_session_manager lib asm execution environment.ex
Raw

lib/asm/execution/environment.ex

defmodule ASM.Execution.Environment do
@moduledoc """
Normalized runtime environment and policy context for ASM execution.
"""
alias ASM.Permission
@valid_approval_postures [:manual, :auto, :none]
@valid_keys [:workspace_root, :allowed_tools, :approval_posture, :permission_mode]
@approval_to_permission %{
manual: :default,
auto: :auto,
none: :bypass
}
@enforce_keys []
defstruct workspace_root: nil,
allowed_tools: [],
approval_posture: nil,
permission_mode: nil
@type t :: %__MODULE__{
workspace_root: String.t() | nil,
allowed_tools: [String.t()],
approval_posture: :manual | :auto | :none | nil,
permission_mode: Permission.normalized_mode() | nil
}
@type validation_error ::
{:unknown_execution_environment_fields, [term()]}
| {:invalid_workspace_root, term()}
| {:invalid_allowed_tools, term()}
| {:invalid_approval_posture, term()}
| {:invalid_permission_mode, term()}
| {:invalid_execution_environment, term()}
@spec keys() :: [atom(), ...]
def keys, do: @valid_keys
@spec new(t() | keyword() | map() | nil) :: {:ok, t()} | {:error, validation_error()}
def new(attrs \\ nil)
def new(nil), do: {:ok, %__MODULE__{}}
def new(%__MODULE__{} = environment), do: {:ok, environment}
def new(attrs) do
with {:ok, normalized_attrs} <- normalize_attrs(attrs) do
{:ok,
struct!(
__MODULE__,
workspace_root: Keyword.get(normalized_attrs, :workspace_root),
allowed_tools: Keyword.get(normalized_attrs, :allowed_tools, []),
approval_posture: Keyword.get(normalized_attrs, :approval_posture),
permission_mode: Keyword.get(normalized_attrs, :permission_mode)
)}
end
end
@spec normalize_attrs(t() | keyword() | map() | nil) ::
{:ok, keyword()} | {:error, validation_error()}
def normalize_attrs(nil), do: {:ok, []}
def normalize_attrs(%__MODULE__{} = environment), do: {:ok, to_attrs(environment)}
def normalize_attrs(attrs) when is_list(attrs) do
if Keyword.keyword?(attrs) do
attrs
|> keyword_present_attrs()
|> normalize_present_attrs()
else
{:error, {:invalid_execution_environment, attrs}}
end
end
def normalize_attrs(attrs) when is_map(attrs) do
attrs
|> map_present_attrs()
|> normalize_present_attrs()
end
def normalize_attrs(other), do: {:error, {:invalid_execution_environment, other}}
@spec to_attrs(t()) :: keyword()
def to_attrs(%__MODULE__{} = environment) do
[
workspace_root: environment.workspace_root,
allowed_tools: environment.allowed_tools,
approval_posture: environment.approval_posture,
permission_mode: environment.permission_mode
]
end
defp keyword_present_attrs(attrs) when is_list(attrs) do
unknown =
attrs
|> Keyword.keys()
|> Enum.uniq()
|> Enum.reject(&(&1 in @valid_keys))
if unknown == [] do
{:ok,
Enum.reduce(@valid_keys, [], fn key, acc ->
if Keyword.has_key?(attrs, key) do
[{key, Keyword.get(attrs, key)} | acc]
else
acc
end
end)
|> Enum.reverse()}
else
{:error, {:unknown_execution_environment_fields, unknown}}
end
end
defp map_present_attrs(attrs) when is_map(attrs) do
attrs
|> Enum.reduce_while({:ok, []}, fn {key, value}, {:ok, acc} ->
case normalize_input_key(key) do
{:ok, normalized_key} ->
{:cont, {:ok, [{normalized_key, value} | acc]}}
:error ->
{:halt, {:error, {:unknown_execution_environment_fields, [key]}}}
end
end)
|> case do
{:ok, values} ->
{:ok, Enum.reverse(values)}
{:error, {:unknown_execution_environment_fields, fields}} ->
{:error, {:unknown_execution_environment_fields, Enum.uniq(fields)}}
end
end
defp normalize_input_key(key) when key in @valid_keys, do: {:ok, key}
defp normalize_input_key(key) when is_binary(key) do
case key do
"workspace_root" -> {:ok, :workspace_root}
"allowed_tools" -> {:ok, :allowed_tools}
"approval_posture" -> {:ok, :approval_posture}
"permission_mode" -> {:ok, :permission_mode}
_other -> :error
end
end
defp normalize_input_key(_key), do: :error
defp normalize_present_attrs({:error, _reason} = error), do: error
defp normalize_present_attrs({:ok, attrs}) when is_list(attrs) do
with {:ok, workspace_root} <- normalize_workspace_root(attrs),
{:ok, allowed_tools} <- normalize_allowed_tools(attrs),
{:ok, approval_posture} <- normalize_approval_posture(attrs),
{:ok, permission_mode} <- normalize_permission_mode(attrs, approval_posture) do
{:ok,
[]
|> maybe_put(:workspace_root, workspace_root, Keyword.has_key?(attrs, :workspace_root))
|> maybe_put(:allowed_tools, allowed_tools, Keyword.has_key?(attrs, :allowed_tools))
|> maybe_put(
:approval_posture,
approval_posture,
Keyword.has_key?(attrs, :approval_posture)
)
|> maybe_put(
:permission_mode,
permission_mode,
Keyword.has_key?(attrs, :permission_mode) or
(Keyword.has_key?(attrs, :approval_posture) and
not Keyword.has_key?(attrs, :permission_mode) and
not is_nil(permission_mode))
)}
end
end
defp normalize_workspace_root(attrs) when is_list(attrs) do
if Keyword.has_key?(attrs, :workspace_root) do
value = Keyword.get(attrs, :workspace_root)
if is_nil(value) or (is_binary(value) and value != "") do
{:ok, value}
else
{:error, {:invalid_workspace_root, value}}
end
else
{:ok, nil}
end
end
defp normalize_allowed_tools(attrs) when is_list(attrs) do
if Keyword.has_key?(attrs, :allowed_tools) do
attrs
|> Keyword.get(:allowed_tools)
|> normalize_string_list()
|> case do
{:ok, values} -> {:ok, values}
{:error, reason} -> {:error, {:invalid_allowed_tools, reason}}
end
else
{:ok, []}
end
end
defp normalize_approval_posture(attrs) when is_list(attrs) do
if Keyword.has_key?(attrs, :approval_posture) do
case normalize_approval_posture_value(Keyword.get(attrs, :approval_posture)) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, {:invalid_approval_posture, reason}}
end
else
{:ok, nil}
end
end
defp normalize_permission_mode(attrs, approval_posture) when is_list(attrs) do
cond do
Keyword.has_key?(attrs, :permission_mode) ->
case normalize_permission_mode_value(Keyword.get(attrs, :permission_mode)) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, {:invalid_permission_mode, reason}}
end
Keyword.has_key?(attrs, :approval_posture) and not is_nil(approval_posture) ->
{:ok, Map.get(@approval_to_permission, approval_posture)}
true ->
{:ok, nil}
end
end
defp normalize_approval_posture_value(nil), do: {:ok, nil}
defp normalize_approval_posture_value(value) when value in @valid_approval_postures,
do: {:ok, value}
defp normalize_approval_posture_value(value) when is_binary(value) do
case String.trim(String.downcase(value)) do
"manual" -> {:ok, :manual}
"auto" -> {:ok, :auto}
"none" -> {:ok, :none}
_other -> {:error, value}
end
end
defp normalize_approval_posture_value(value), do: {:error, value}
defp normalize_permission_mode_value(nil), do: {:ok, nil}
defp normalize_permission_mode_value(value) when is_atom(value) do
if value in Permission.normalized_modes() do
{:ok, value}
else
{:error, value}
end
end
defp normalize_permission_mode_value(value) when is_binary(value) do
value
|> String.trim()
|> String.downcase()
|> case do
"default" -> {:ok, :default}
"auto" -> {:ok, :auto}
"bypass" -> {:ok, :bypass}
"plan" -> {:ok, :plan}
_other -> {:error, value}
end
end
defp normalize_permission_mode_value(value), do: {:error, value}
defp normalize_string_list(values) when is_list(values) do
Enum.reduce_while(values, {:ok, {[], MapSet.new()}}, fn
value, {:ok, {acc, seen}} when is_binary(value) ->
trimmed = String.trim(value)
cond do
trimmed == "" ->
{:halt, {:error, :empty_entry}}
MapSet.member?(seen, trimmed) ->
{:cont, {:ok, {acc, seen}}}
true ->
{:cont, {:ok, {acc ++ [trimmed], MapSet.put(seen, trimmed)}}}
end
value, {:ok, {acc, seen}} when is_atom(value) ->
normalized = Atom.to_string(value)
if MapSet.member?(seen, normalized) do
{:cont, {:ok, {acc, seen}}}
else
{:cont, {:ok, {acc ++ [normalized], MapSet.put(seen, normalized)}}}
end
value, _acc ->
{:halt, {:error, {:invalid_entry, value}}}
end)
|> case do
{:ok, {normalized, _seen}} -> {:ok, normalized}
{:error, reason} -> {:error, reason}
end
end
defp normalize_string_list(value), do: {:error, {:invalid_list, value}}
defp maybe_put(attrs, _key, _value, false), do: attrs
defp maybe_put(attrs, key, value, true), do: Keyword.put(attrs, key, value)
end