lib/openagents/computer_projection.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.ComputerProjection do
  @moduledoc """
  Safe projection of a paired computer for API and product surfaces.

  Everything here is the owner's own record of their own computer. What stays
  out is what IDENTITY-008 keeps out: the computer token, its digest, and the
  raw probe document.

  `revoked_at` is included because `status` alone answers half the question.
  Every other credential in this release publishes both — `api_tokens`,
  `inference_grants`, `agent_tokens`, `forge_assignment_credentials` — and this
  was the one terminal stamp the database held and nothing read, so an account
  export could tell you a computer was revoked and never when.
  """

  alias OpenAgents.Computer
  alias OpenAgents.Machines.Machine

  @spec project(Machine.t()) :: map()
  def project(%Machine{} = machine) do
    %{
      "id" => machine.id,
      "name" => machine.name,
      "tier" => machine.tier,
      "status" => machine.status,
      "revoked_at" => iso8601(machine.revoked_at),
      "scoped_forge_credentials_enabled" => machine.scoped_forge_credentials_enabled,
      "online" => machine.status == "active" and Computer.online?(machine.id),
      "platform" => machine.platform,
      "agent_version" => machine.agent_version,
      "roots" => machine.roots,
      "last_seen_at" => iso8601(machine.last_seen_at),
      "acp_agents" => acp_agents(machine.last_probe)
    }
  end

  defp acp_agents(%{"acp_agents" => agents}) when is_list(agents) do
    agents
    |> Enum.take(16)
    |> Enum.flat_map(fn
      %{"id" => id} = agent when is_binary(id) and id != "" ->
        [
          %{
            "id" => String.slice(id, 0, 64),
            "version" => bounded(agent["version"], 80),
            "source" => bounded(agent["source"], 40),
            "auth_ready" => boolean_or_nil(agent["auth_ready"]),
            "model" => bounded(agent["model"], 128),
            "reasoning_effort" => bounded(agent["reasoning_effort"], 32),
            "mode" => bounded(agent["mode"], 32)
          }
        ]

      _invalid ->
        []
    end)
  end

  defp acp_agents(_probe), do: []
  defp iso8601(%DateTime{} = value), do: DateTime.to_iso8601(value)
  defp iso8601(_value), do: nil
  defp bounded(value, maximum) when is_binary(value), do: String.slice(value, 0, maximum)
  defp bounded(_value, _maximum), do: nil
  defp boolean_or_nil(value) when is_boolean(value), do: value
  defp boolean_or_nil(_value), do: nil
end