lib/openagents/tools/repo_list.ex

58e6347eeb72 · 2 KB

defmodule OpenAgents.Tools.RepoList do
  @moduledoc "Lists a directory of OpenAgents source tree, bounded."

  @behaviour OpenAgents.Tools.Tool

  alias OpenAgents.Modules.Metadata
  alias OpenAgents.Tools.{ExecutionResult, Repository, Tool}

  @max_entries 500
  @skip ~w(.git _build deps node_modules)

  @impl true
  def specification do
    %Tool{
      module_id: "sarah.tool.repo_list.v1",
      name: "repo_list",
      version: 1,
      description:
        "Lists files and directories in OpenAgents source tree. Pass path relative to the " <>
          "repository root (default: the root). from selects \"image\" (running source, " <>
          "default) or \"workspace\" (this job's clone).",
      input_schema: %{
        "type" => "object",
        "properties" => %{
          "path" => %{"type" => "string", "maxLength" => 512},
          "from" => %{"type" => "string", "enum" => ["image", "workspace"]}
        },
        "additionalProperties" => false
      },
      output_schema: %{"type" => "object", "properties" => %{}, "additionalProperties" => true},
      side_effect: :read_only,
      required_scope: "browser_conversation",
      required_authority: "repository.read",
      executor: %{
        id: "sarah.repository.self",
        disclosure: "the OpenAgents runtime, listing her running source"
      },
      maintainer: "OpenAgents",
      attribution: ["OpenAgentsInc/openagents.com"],
      policy_facets: %{
        "privacy" => "browser_conversation",
        "residency" => "host",
        "consent" => "not_applicable"
      },
      module_metadata:
        Metadata.first_party("repository.read", "browser_conversation",
          effect: :read_only,
          privacy: "browser_conversation",
          residency: "host",
          surfaces: ["text"]
        ),
      timeout_ms: 10_000,
      maximum_input_bytes: 2_048,
      maximum_output_bytes: 65_536,
      implementation: __MODULE__
    }
  end

  @impl true
  def execute(arguments, context) when is_map(arguments) do
    path = Map.get(arguments, "path", ".")

    with {:ok, root} <- Repository.tool_root(Map.get(arguments, "from", "image"), context),
         {:ok, resolved} <- Repository.safe_path(root, path) do
      if File.dir?(resolved) do
        entries =
          resolved
          |> File.ls!()
          |> Enum.sort()
          |> Enum.reject(&(&1 in @skip))
          |> Enum.take(@max_entries)
          |> Enum.map(fn name ->
            full = Path.join(resolved, name)

            %{
              "name" => name,
              "type" => if(File.dir?(full), do: "directory", else: "file")
            }
          end)

        {:ok,
         %ExecutionResult{
           result: %{
             "schema" => "sarah.repo_list_result.v1",
             "path" => path,
             "entries" => entries
           }
         }}
      else
        {:error, :repository_file_not_found}
      end
    end
  end

  def execute(_arguments, _context), do: {:error, :invalid_repository_path}
end