lib/openagents/roles/general_collaborator.ex

58e6347eeb72 · 2 KB

defmodule OpenAgents.Roles.GeneralCollaborator do
  @moduledoc "The only role program admitted by the initial public application."

  @behaviour OpenAgents.Roles.Role

  alias OpenAgents.Roles.Role
  alias OpenAgents.Provenance.Canonical

  @id "sarah.role.general_collaborator.v1"
  @version 1
  @status "admitted"
  @source_ref "release-artifact:sarah.role.general_collaborator.v1"
  @compatibility_min 1
  @compatibility_max 1
  @surfaces ["text", "voice"]
  @required_capabilities []
  @register "ordinary_collaboration"
  @content """
  Work as the person's expert peer and general collaborator. Understand the
  actual question, give the answer or decision first, preserve important nuance,
  and offer the smallest useful next action. Ask one concrete question only when
  it is necessary to proceed.

  Do not silently enter sales, company-operations, coding-agent, or public-broadcast
  mode. Those require separately admitted roles and capabilities. Never pressure
  the person toward a sale, and do not use military framing in ordinary chat.
  """
  @admitted_digest "920c9d3ea657ecd08337755350e69a71805fee2a6282f30526c75dbcba59b813"

  @impl true
  def artifact do
    content = String.trim(@content)
    digest = calculated_digest()

    if digest != @admitted_digest do
      raise ArgumentError, "general collaborator role digest is not admitted"
    end

    %Role{
      id: @id,
      version: @version,
      status: @status,
      content: content,
      digest: digest,
      source_ref: @source_ref,
      source_digest: source_digest(),
      compatibility_min: @compatibility_min,
      compatibility_max: @compatibility_max,
      surfaces: @surfaces,
      required_capabilities: @required_capabilities,
      register: @register
    }
  end

  @doc false
  def calculated_digest do
    Canonical.digest!(%{
      "schema" => "sarah.role_program.v1",
      "id" => @id,
      "version" => @version,
      "status" => @status,
      "content" => String.trim(@content),
      "source_ref" => @source_ref,
      "source_digest" => source_digest(),
      "compatibility_min" => @compatibility_min,
      "compatibility_max" => @compatibility_max,
      "surfaces" => @surfaces,
      "required_capabilities" => @required_capabilities,
      "register" => @register
    })
  end

  defp source_digest, do: calculate_digest(String.trim(@content))

  @doc false
  def calculate_digest(content) when is_binary(content) do
    content
    |> then(&:crypto.hash(:sha256, &1))
    |> Base.encode16(case: :lower)
  end
end