Current section
Files
Jump to
Current section
Files
lib/codex/app_server/approval_decision.ex
defmodule Codex.AppServer.ApprovalDecision do
@moduledoc false
alias Codex.Protocol.RequestPermissions
@type wire_decision :: String.t() | map()
@spec from_hook(term()) :: wire_decision()
def from_hook(:allow), do: "accept"
def from_hook({:allow, opts}) when is_list(opts) do
cond do
Keyword.get(opts, :for_session, false) ->
"acceptForSession"
Keyword.get(opts, :grant_root) not in [nil, false] ->
"acceptForSession"
execpolicy_amendment = Keyword.get(opts, :execpolicy_amendment) ->
%{
"acceptWithExecpolicyAmendment" => %{
"execpolicyAmendment" => List.wrap(execpolicy_amendment)
}
}
true ->
"accept"
end
end
def from_hook({:deny, reason}) when reason in [:cancel, "cancel"], do: "cancel"
def from_hook({:deny, _reason}), do: "decline"
def from_hook(other) when other in [:decline, :cancel, "decline", "cancel"] do
to_string(other)
end
def from_hook(_), do: "decline"
@spec from_permissions_hook(
term(),
RequestPermissions.RequestPermissionProfile.t() | map() | nil
) :: map()
def from_permissions_hook(decision, requested_permissions) do
requested = RequestPermissions.RequestPermissionProfile.from_map(requested_permissions)
case decision do
:allow ->
requested
|> grant_all_requested()
|> permissions_response(:turn)
{:allow, opts} when is_list(opts) ->
permissions =
opts
|> Keyword.get(:permissions, requested)
|> intersect_requested_permissions(requested)
scope = Keyword.get(opts, :scope, :turn)
permissions_response(permissions, scope)
{:deny, _reason} ->
permissions_response(%RequestPermissions.GrantedPermissionProfile{}, :turn)
_ ->
permissions_response(%RequestPermissions.GrantedPermissionProfile{}, :turn)
end
end
defp permissions_response(%RequestPermissions.GrantedPermissionProfile{} = permissions, scope) do
%RequestPermissions.Response{permissions: permissions, scope: normalize_scope(scope)}
|> RequestPermissions.Response.to_map()
end
defp normalize_scope(:session), do: :session
defp normalize_scope("session"), do: :session
defp normalize_scope(_), do: :turn
defp grant_all_requested(%RequestPermissions.RequestPermissionProfile{} = requested) do
%RequestPermissions.GrantedPermissionProfile{
network: copy_network_permissions(requested.network),
file_system: copy_file_system_permissions(requested.file_system),
macos: copy_macos_permissions(requested.macos)
}
end
defp intersect_requested_permissions(granted_permissions, requested_permissions) do
granted = RequestPermissions.GrantedPermissionProfile.from_map(granted_permissions)
%RequestPermissions.GrantedPermissionProfile{
network: intersect_network_permissions(requested_permissions.network, granted.network),
file_system:
intersect_file_system_permissions(requested_permissions.file_system, granted.file_system),
macos: intersect_macos_permissions(requested_permissions.macos, granted.macos)
}
end
defp intersect_network_permissions(
%RequestPermissions.AdditionalNetworkPermissions{enabled: true},
%RequestPermissions.AdditionalNetworkPermissions{enabled: true}
) do
%RequestPermissions.AdditionalNetworkPermissions{enabled: true}
end
defp intersect_network_permissions(_requested, _granted), do: nil
defp intersect_file_system_permissions(
%RequestPermissions.AdditionalFileSystemPermissions{} = requested,
%RequestPermissions.AdditionalFileSystemPermissions{} = granted
) do
%RequestPermissions.AdditionalFileSystemPermissions{
read: intersect_path_lists(requested.read, granted.read),
write: intersect_path_lists(requested.write, granted.write)
}
|> empty_file_system_permissions_to_nil()
end
defp intersect_file_system_permissions(_requested, _granted), do: nil
defp intersect_macos_permissions(
%RequestPermissions.AdditionalMacOsPermissions{} = requested,
%RequestPermissions.GrantedMacOsPermissions{} = granted
) do
%RequestPermissions.GrantedMacOsPermissions{
preferences: intersect_permission_level(requested.preferences, granted.preferences),
automations: intersect_automation_permissions(requested.automations, granted.automations),
launch_services:
intersect_boolean_permission(requested.launch_services, granted.launch_services),
accessibility: intersect_boolean_permission(requested.accessibility, granted.accessibility),
calendar: intersect_boolean_permission(requested.calendar, granted.calendar),
reminders: intersect_boolean_permission(requested.reminders, granted.reminders),
contacts: intersect_permission_level(requested.contacts, granted.contacts)
}
|> empty_macos_permissions_to_nil()
end
defp intersect_macos_permissions(_requested, _granted), do: nil
defp intersect_path_lists(nil, _granted), do: nil
defp intersect_path_lists(_requested, nil), do: nil
defp intersect_path_lists(requested, granted) when is_list(requested) and is_list(granted) do
allowed = MapSet.new(granted)
values = Enum.filter(requested, &MapSet.member?(allowed, &1))
if values == [], do: nil, else: values
end
defp copy_network_permissions(nil), do: nil
defp copy_network_permissions(%RequestPermissions.AdditionalNetworkPermissions{} = permissions) do
%RequestPermissions.AdditionalNetworkPermissions{enabled: permissions.enabled}
end
defp copy_file_system_permissions(nil), do: nil
defp copy_file_system_permissions(
%RequestPermissions.AdditionalFileSystemPermissions{} = permissions
) do
%RequestPermissions.AdditionalFileSystemPermissions{
read: permissions.read,
write: permissions.write
}
end
defp copy_macos_permissions(nil), do: nil
defp copy_macos_permissions(%RequestPermissions.AdditionalMacOsPermissions{} = permissions) do
%RequestPermissions.GrantedMacOsPermissions{
preferences: normalize_permission_level(permissions.preferences),
automations: copy_automation_permissions(permissions.automations),
launch_services: permissions.launch_services,
accessibility: permissions.accessibility,
calendar: permissions.calendar,
reminders: permissions.reminders,
contacts: normalize_permission_level(permissions.contacts)
}
|> empty_macos_permissions_to_nil()
end
defp empty_file_system_permissions_to_nil(%RequestPermissions.AdditionalFileSystemPermissions{
read: nil,
write: nil
}),
do: nil
defp empty_file_system_permissions_to_nil(permissions), do: permissions
defp empty_macos_permissions_to_nil(%RequestPermissions.GrantedMacOsPermissions{} = permissions) do
if Enum.all?(
[
permissions.preferences,
permissions.automations,
permissions.launch_services,
permissions.accessibility,
permissions.calendar,
permissions.reminders,
permissions.contacts
],
&is_nil/1
) do
nil
else
permissions
end
end
defp intersect_boolean_permission(true, true), do: true
defp intersect_boolean_permission(_, _), do: nil
defp intersect_permission_level(requested, granted) do
case {permission_level_rank(requested), permission_level_rank(granted)} do
{req, grant} when is_integer(req) and is_integer(grant) and req > 0 and grant > 0 ->
min(req, grant) |> permission_level_from_rank()
_ ->
nil
end
end
defp copy_automation_permissions(nil), do: nil
defp copy_automation_permissions(%{} = value), do: value
defp copy_automation_permissions(value) when is_binary(value),
do: normalize_automation_permission(value)
defp intersect_automation_permissions(requested, granted) do
requested = normalize_automation_permission(requested)
granted = normalize_automation_permission(granted)
do_intersect_automation_permissions(requested, granted)
end
defp do_intersect_automation_permissions("all", "all"), do: "all"
defp do_intersect_automation_permissions("all", %{"bundle_ids" => bundle_ids})
when is_list(bundle_ids) and bundle_ids != [] do
%{"bundle_ids" => bundle_ids}
end
defp do_intersect_automation_permissions(%{"bundle_ids" => bundle_ids}, "all")
when is_list(bundle_ids) and bundle_ids != [] do
%{"bundle_ids" => bundle_ids}
end
defp do_intersect_automation_permissions(
%{"bundle_ids" => requested_ids},
%{"bundle_ids" => granted_ids}
)
when is_list(requested_ids) and is_list(granted_ids) do
granted = MapSet.new(granted_ids)
bundle_ids = Enum.filter(requested_ids, &MapSet.member?(granted, &1))
if bundle_ids == [], do: nil, else: %{"bundle_ids" => bundle_ids}
end
defp do_intersect_automation_permissions(_requested, _granted), do: nil
defp normalize_permission_level(nil), do: nil
defp normalize_permission_level(value) when is_binary(value) do
case value do
"none" -> nil
"read_only" -> "read_only"
"read_write" -> "read_write"
_ -> nil
end
end
defp permission_level_rank(value) do
case normalize_permission_level(value) do
"read_only" -> 1
"read_write" -> 2
_ -> 0
end
end
defp permission_level_from_rank(1), do: "read_only"
defp permission_level_from_rank(2), do: "read_write"
defp normalize_automation_permission(nil), do: nil
defp normalize_automation_permission("none"), do: nil
defp normalize_automation_permission("all"), do: "all"
defp normalize_automation_permission(%{"bundle_ids" => bundle_ids}) when is_list(bundle_ids),
do: %{"bundle_ids" => bundle_ids}
defp normalize_automation_permission(%{bundle_ids: bundle_ids}) when is_list(bundle_ids),
do: %{"bundle_ids" => bundle_ids}
defp normalize_automation_permission(value) when is_binary(value), do: value
defp normalize_automation_permission(value),
do: value |> to_string() |> normalize_automation_permission()
end