defmodule Codex.GovernedAuthority do @moduledoc false alias Codex.Runtime.Env, as: RuntimeEnv @required_ref_keys ~w( authority_ref credential_lease_ref native_auth_assertion_ref connector_instance_ref provider_account_ref connector_binding_ref target_ref operation_policy_ref materialization_ref ) @command_ref_keys ~w(command_ref command_materialization_ref provider_command_ref) @secret_key_terms ~w(api_key apikey token secret password bearer authorization refresh access) @allowed_secret_key_names ~w( authority_ref credential_lease_ref native_auth_assertion_ref connector_instance_ref provider_account_ref connector_binding_ref target_ref operation_policy_ref materialization_ref command_ref command_materialization_ref provider_command_ref ) @type t :: %{required(String.t()) => String.t()} @spec normalize(term()) :: {:ok, t() | nil} | {:error, term()} def normalize(nil), do: {:ok, nil} def normalize(authority) when is_list(authority) do if Keyword.keyword?(authority) do authority |> Map.new() |> normalize() else {:error, {:invalid_governed_authority, authority}} end end def normalize(%{} = authority) do normalized = Map.new(authority, fn {key, value} -> {normalize_key(key), value} end) with :ok <- validate_required_refs(normalized), :ok <- reject_secret_key_names(normalized) do {:ok, normalized} end end def normalize(authority), do: {:error, {:invalid_governed_authority, authority}} @spec fetch(map() | keyword()) :: {:ok, t() | nil} | {:error, term()} def fetch(attrs) when is_list(attrs), do: attrs |> Map.new() |> fetch() def fetch(%{} = attrs) do attrs |> fetch_first([:governed_authority, "governed_authority", :authority_refs, "authority_refs"]) |> normalize() end @spec present?(term()) :: boolean() def present?(nil), do: false def present?(%{}), do: true def present?(_value), do: false @spec validate_clear_env(t() | nil, term(), atom()) :: :ok | {:error, term()} def validate_clear_env(nil, _clear_env?, _surface), do: :ok def validate_clear_env(_authority, true, _surface), do: :ok def validate_clear_env(_authority, clear_env?, surface) do {:error, {:governed_clear_env_required, surface, clear_env?}} end @spec validate_runtime_env(t() | nil, map()) :: :ok | {:error, term()} def validate_runtime_env(nil, _materialized_env), do: :ok def validate_runtime_env(%{} = authority, materialized_env) when is_map(materialized_env) do validate_required_refs(authority) end @spec validate_command_override(t() | nil, String.t() | nil, atom()) :: :ok | {:error, term()} def validate_command_override(nil, _command_override, _surface), do: :ok def validate_command_override(_authority, nil, _surface), do: :ok def validate_command_override(_authority, "", _surface), do: :ok def validate_command_override(%{} = authority, command_override, surface) when is_binary(command_override) do if Enum.any?(@command_ref_keys, &ref_present?(authority, &1)) do :ok else {:error, {:governed_command_ref_required, surface}} end end @spec reject_config_overrides(t() | nil, list(), atom()) :: :ok | {:error, term()} def reject_config_overrides(nil, _overrides, _surface), do: :ok def reject_config_overrides(_authority, nil, _surface), do: :ok def reject_config_overrides(%{} = _authority, overrides, surface) when is_list(overrides) do case Enum.find_value(overrides, &secret_config_key/1) do nil -> :ok key -> {:error, {:governed_secret_config_override, surface, key}} end end @spec governed_child_env(keyword(), atom()) :: {:ok, map()} | {:error, term()} def governed_child_env(opts, surface) when is_list(opts) do with {:ok, authority} <- fetch(opts), {:ok, env} <- normalize_process_env(opts), :ok <- validate_runtime_env(authority, env), :ok <- require_codex_home(authority, env, opts, surface) do {:ok, maybe_merge_system_env(authority, env)} end end defp validate_required_refs(authority) do missing = Enum.reject(@required_ref_keys, &ref_present?(authority, &1)) case missing do [] -> :ok _ -> {:error, {:missing_governed_authority_refs, missing}} end end defp reject_secret_key_names(authority) do authority |> flatten_keys() |> Enum.find(&secret_key_name?/1) |> case do nil -> :ok key -> {:error, {:raw_secret_field_in_governed_authority, key}} end end defp require_codex_home(nil, _env, _opts, _surface), do: :ok defp require_codex_home(%{} = _authority, env, opts, surface) do codex_home = fetch_first(opts, [:codex_home, "codex_home"]) || Map.get(env, "CODEX_HOME") case normalize_env_value(codex_home) do nil -> {:error, {:governed_codex_home_required, surface}} _value -> :ok end end defp maybe_merge_system_env(nil, env), do: env defp maybe_merge_system_env(%{}, env), do: env defp normalize_process_env(opts) do opts |> Keyword.get(:process_env, Keyword.get(opts, :env, %{})) |> RuntimeEnv.normalize_overrides() end defp secret_config_key({key, _value}), do: secret_config_key(key) defp secret_config_key(value) when is_binary(value) do key = value |> String.split("=", parts: 2) |> List.first() |> normalize_key() if secret_key_name?(key), do: key end defp secret_config_key(value), do: secret_config_key(to_string(value)) defp secret_key_name?(key) do key = normalize_key(key) if key in @allowed_secret_key_names do false else Enum.any?(@secret_key_terms, &String.contains?(key, &1)) end end defp flatten_keys(%{} = map) do Enum.flat_map(map, fn {key, value} -> [normalize_key(key) | flatten_keys(value)] end) end defp flatten_keys(list) when is_list(list) do if Keyword.keyword?(list) do flatten_keys(Map.new(list)) else [] end end defp flatten_keys(_value), do: [] defp ref_present?(authority, key) do case Map.get(authority, key) do value when is_binary(value) -> String.trim(value) != "" _ -> false end end defp fetch_first(attrs, keys) when is_list(attrs) do attrs |> Map.new() |> fetch_first(keys) end defp fetch_first(attrs, [key | rest]) do case Map.get(attrs, key) do nil -> fetch_first(attrs, rest) value -> value end end defp fetch_first(_attrs, []), do: nil defp normalize_key(key) when is_atom(key), do: key |> Atom.to_string() |> normalize_key() defp normalize_key(key) when is_binary(key), do: key |> String.trim() |> String.downcase() defp normalize_key(key), do: key |> to_string() |> normalize_key() defp normalize_env_value(nil), do: nil defp normalize_env_value(value) when is_binary(value) do value = String.trim(value) if value == "", do: nil, else: value end defp normalize_env_value(value), do: value |> to_string() |> normalize_env_value() end