lib/openagents/plugins/forge_source.ex

58e6347eeb72 · 3 KB

defmodule OpenAgents.Plugins.ForgeSource do
  @moduledoc """
  Adapts the plugin index over forge repository data.

  The forge already stores repository identity and git objects. This source is
  the smallest discoverable-release adapter: it walks the public, ready
  repositories and reads `manifest.json` from the default branch. A missing
  manifest is not an error; a plugin registry entry only appears when a
  repository publishes a valid typed manifest.

  The artifact digest is the manifest's declared `artifact.digest`; this layer
  does not fetch or verify the artifact bytes. That remains the runtime's job
  when a caller selects a plugin by exact name and installs it.
  """

  import Ecto.Query, warn: false

  alias OpenAgents.Plugins.Index

  @doc """
  Return index entries for every repository an anonymous reader can see that
  has a `manifest.json` on its default branch.

  The population composes `OpenAgents.Repositories.readable_by/2` with no user
  rather than restating the join (REPOSITORY-001). The registry is a public
  listing, so it must name exactly the repositories an anonymous reader
  already reaches — a private repository publishing a plugin manifest here
  would contradict TRANSPARENCY-001 the same way a published anchor would.
  """
  @spec entries() :: [Index.Entry.t()]
  def entries do
    OpenAgents.Repositories.Repository
    |> OpenAgents.Repositories.readable_by(nil)
    |> OpenAgents.Repo.all()
    |> Enum.flat_map(&entries_for_repository/1)
  end

  defp entries_for_repository(
         %{
           owner: owner,
           name: name,
           storage_key: storage_key,
           default_branch: branch
         } = repository
       )
       when is_binary(owner) and is_binary(name) and is_binary(storage_key) and is_binary(branch) do
    path = bare_path(repository)
    release = default_release(repository)

    case read_manifest_at_head(path, repository) do
      {:ok, manifest} ->
        [
          %Index.Entry{
            repository: display_path(repository),
            release: release,
            manifest: manifest
          }
        ]

      {:error, _reason} ->
        []
    end
  end

  defp entries_for_repository(_), do: []

  defp read_manifest_at_head(path, repository) do
    with refs when is_map(refs) <- OpenAgents.Forge.Repos.refs_at(path),
         sha when is_binary(sha) <- Map.get(refs, "refs/heads/#{repository.default_branch}") do
      read_manifest(path, sha)
    else
      _ ->
        {:error, :no_head}
    end
  end

  defp read_manifest(path, sha) do
    case OpenAgents.Forge.Repos.git(path, ["show", "#{sha}:manifest.json"]) do
      {output, 0} ->
        case Jason.decode(output) do
          {:ok, manifest} when is_map(manifest) ->
            {:ok, manifest}

          {:error, %Jason.DecodeError{}} ->
            {:error, :invalid_json}
        end

      _error ->
        {:error, :manifest_not_found}
    end
  end

  defp display_path(%{owner: owner, name: name}), do: "#{owner}/#{name}"

  defp bare_path(%{storage_key: storage_key}), do: OpenAgents.Forge.Repos.bare_path(storage_key)

  defp default_release(%{default_branch: branch}), do: branch
end