lib/openagents/tools/github_repo_list.ex

main at 58e6347eeb72 · 3 KB

defmodule OpenAgents.Tools.GitHubRepoList do
  @moduledoc "Lists GitHub repositories the signed-in user can access, using their 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_list.v1",
      name: "github_repo_list",
      version: 1,
      description:
        "Lists GitHub repositories the signed-in user owns or collaborates on, most recently " <>
          "pushed first. Use this whenever the user asks about their repos or projects on " <>
          "GitHub. Pass first as the number of repositories to return (1-50).",
      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: 1_024,
      maximum_output_bytes: 65_536,
      implementation: __MODULE__
    }
  end

  @impl true
  def execute(%{"first" => first}, context) when is_integer(first) and first in 1..50 do
    with {:ok, token, user} <- GitHubContext.resolve(context),
         {:ok, repositories} <- GitHub.list_repositories(token, first: first) do
      {:ok,
       %ExecutionResult{
         result: %{
           "schema" => "sarah.github_repo_list_result.v1",
           "scope" => "signed_github_user",
           "github_login" => user.github_login,
           "status" => if(repositories == [], do: "empty", else: "matches"),
           "repositories" => repositories
         },
         target_receipt_refs: Enum.map(repositories, &"github-repo:#{&1["full_name"]}")
       }}
    end
  end

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

  defp input_schema do
    %{
      "type" => "object",
      "properties" => %{
        "first" => %{"type" => "integer", "minimum" => 1, "maximum" => 50}
      },
      "required" => ["first"],
      "additionalProperties" => false
    }
  end

  defp output_schema do
    repository_schema = %{
      "type" => "object",
      "properties" => %{
        "full_name" => string(140),
        "description" => string(300),
        "private" => %{"type" => "boolean"},
        "default_branch" => string(100),
        "language" => string(60),
        "pushed_at" => string(32)
      },
      "required" => [
        "full_name",
        "description",
        "private",
        "default_branch",
        "language",
        "pushed_at"
      ],
      "additionalProperties" => false
    }

    %{
      "type" => "object",
      "properties" => %{
        "schema" => string(64),
        "scope" => string(32),
        "github_login" => string(64),
        "status" => string(16),
        "repositories" => %{"type" => "array", "maxItems" => 50, "items" => repository_schema}
      },
      "required" => ["schema", "scope", "github_login", "status", "repositories"],
      "additionalProperties" => false
    }
  end

  defp string(maximum), do: %{"type" => "string", "maxLength" => maximum}
end