lib/openagents/tools/repo_read.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Tools.RepoRead do
  @moduledoc "Reads one file from OpenAgents source (baked image tree or the job's clone)."

  @behaviour OpenAgents.Tools.Tool

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

  @max_bytes 200_000

  @impl true
  def specification do
    %Tool{
      module_id: "sarah.tool.repo_read.v1",
      name: "repo_read",
      version: 1,
      description:
        "Reads one file from OpenAgents source code. Pass path relative to the repository " <>
          "root. from selects the tree: \"image\" (default) reads the source of the code " <>
          "currently running; \"workspace\" reads this job's editable clone.",
      input_schema: %{
        "type" => "object",
        "properties" => %{
          "path" => %{"type" => "string", "maxLength" => 512},
          "from" => %{"type" => "string", "enum" => ["image", "workspace"]}
        },
        "required" => ["path"],
        "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, reading 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: 262_144,
      implementation: __MODULE__
    }
  end

  @impl true
  def execute(%{"path" => path} = arguments, context) when is_binary(path) do
    with {:ok, root} <- Repository.tool_root(Map.get(arguments, "from", "image"), context),
         {:ok, resolved} <- Repository.safe_path(root, path) do
      cond do
        not File.regular?(resolved) ->
          {:error, :repository_file_not_found}

        true ->
          content = File.read!(resolved)
          truncated = byte_size(content) > @max_bytes

          {:ok,
           %ExecutionResult{
             result: %{
               "schema" => "sarah.repo_read_result.v1",
               "path" => path,
               "from" => Map.get(arguments, "from", "image"),
               "content" => binary_slice(content, 0, @max_bytes),
               "bytes" => byte_size(content),
               "truncated" => truncated
             }
           }}
      end
    end
  end

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