lib/openagents/tools/github_repo_read.ex

58e6347eeb72 · 3 KB

defmodule OpenAgents.Tools.GitHubRepoRead do
  @moduledoc "Reads a file or directory from a GitHub repository with the user's OAuth token."

  @behaviour OpenAgents.Tools.Tool

  alias OpenAgents.GitHub
  alias OpenAgents.Modules.Metadata
  alias OpenAgents.Tools.{ExecutionResult, GitHubContext, Tool}

  @impl true
  def specification do
    %Tool{
      module_id: "sarah.tool.github_repo_read.v1",
      name: "github_repo_read",
      version: 1,
      description:
        "Reads a file or lists a directory inside a GitHub repository the signed-in user can " <>
          "access. Pass repository as owner/name, path as a repo-relative path (empty string " <>
          "for the root), and ref as a branch, tag, or commit (empty string for the default " <>
          "branch).",
      input_schema: input_schema(),
      output_schema: output_schema(),
      side_effect: :read_only,
      required_scope: "browser_conversation",
      required_authority: "github.read",
      executor: %{id: "sarah.github.api", disclosure: "GitHub API with the user's authorization"},
      maintainer: "OpenAgents",
      attribution: ["OpenAgentsInc/openagents.com"],
      policy_facets: %{
        "privacy" => "signed_browser_owner",
        "residency" => "application_process",
        "consent" => "not_applicable"
      },
      module_metadata:
        Metadata.first_party("github.read", "browser_conversation",
          effect: :read_only,
          privacy: "signed_browser_owner",
          residency: "application_process"
        ),
      timeout_ms: 15_000,
      maximum_input_bytes: 2_048,
      maximum_output_bytes: 131_072,
      implementation: __MODULE__
    }
  end

  @impl true
  def execute(%{"repository" => repository, "path" => path, "ref" => ref}, context)
      when is_binary(repository) and is_binary(path) and is_binary(ref) do
    with {:ok, token, _user} <- GitHubContext.resolve(context),
         {:ok, contents} <- GitHub.read_path(token, repository, path, ref) do
      {:ok,
       %ExecutionResult{
         result:
           Map.merge(contents, %{
             "schema" => "sarah.github_repo_read_result.v1",
             "scope" => "signed_github_user",
             "repository" => repository
           }),
         target_receipt_refs: ["github-repo:#{repository}"]
       }}
    end
  end

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

  defp input_schema do
    %{
      "type" => "object",
      "properties" => %{
        "repository" => %{"type" => "string", "maxLength" => 140},
        "path" => %{"type" => "string", "maxLength" => 500},
        "ref" => %{"type" => "string", "maxLength" => 100}
      },
      "required" => ["repository", "path", "ref"],
      "additionalProperties" => false
    }
  end

  defp output_schema do
    entry_schema = %{
      "type" => "object",
      "properties" => %{
        "name" => %{"type" => "string", "maxLength" => 255},
        "path" => %{"type" => "string", "maxLength" => 500},
        "type" => %{"type" => "string", "maxLength" => 16},
        "size" => %{"type" => "integer"}
      },
      "required" => ["name", "path", "type", "size"],
      "additionalProperties" => false
    }

    %{
      "type" => "object",
      "properties" => %{
        "schema" => %{"type" => "string", "maxLength" => 64},
        "scope" => %{"type" => "string", "maxLength" => 32},
        "repository" => %{"type" => "string", "maxLength" => 140},
        "type" => %{"type" => "string", "maxLength" => 16},
        "path" => %{"type" => "string", "maxLength" => 500},
        "size" => %{"type" => "integer"},
        "truncated" => %{"type" => "boolean"},
        "content" => %{"type" => "string", "maxLength" => 65_536},
        "entries" => %{"type" => "array", "maxItems" => 200, "items" => entry_schema}
      },
      "required" => ["schema", "scope", "repository", "type"],
      "additionalProperties" => false
    }
  end
end