lib/openagents/tools/redaction.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgents.Tools.Redaction do
  @moduledoc "Redacts credential-shaped fields before tool data reaches providers, clients, or storage."

  @redacted "[REDACTED]"
  @sensitive_fragments ~w(api_key authorization cookie credential password private_key secret token)
  @secret_patterns [
    ~r/\bsk-(?:or-v1-)?[A-Za-z0-9_-]{16,}\b/,
    ~r/\boa_(?:pat|agent|assignment)_[A-Za-z0-9._-]{16,}\b/,
    ~r/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+=*\b/i,
    ~r/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/
  ]

  @spec redact(term()) :: term()
  def redact(value) when is_map(value) do
    Map.new(value, fn {key, nested} ->
      if sensitive_key?(key), do: {key, @redacted}, else: {key, redact(nested)}
    end)
  end

  def redact(value) when is_list(value), do: Enum.map(value, &redact/1)

  def redact(value) when is_tuple(value),
    do: value |> Tuple.to_list() |> Enum.map(&redact/1) |> List.to_tuple()

  def redact(value), do: value

  @spec redact_text(String.t()) :: String.t()
  def redact_text(value) when is_binary(value) do
    Enum.reduce(@secret_patterns, value, &Regex.replace(&1, &2, @redacted))
  end

  defp sensitive_key?(key) when is_atom(key), do: key |> Atom.to_string() |> sensitive_key?()

  defp sensitive_key?(key) when is_binary(key) do
    normalized = String.downcase(key)
    Enum.any?(@sensitive_fragments, &String.contains?(normalized, &1))
  end

  defp sensitive_key?(_key), do: false
end