lib/openagents/program_artifacts/reader.ex

58e6347eeb72 · 11 KB

defmodule OpenAgents.ProgramArtifacts.Reader do
  @moduledoc """
  Pure validator and canonical reader for DS-Effect-style program artifacts.

  The reader returns data. It has no executor, tool, registry, persona,
  persistence, provider, approval, or promotion callback.
  """

  alias OpenAgents.ProgramArtifacts.Artifact
  alias OpenAgents.Provenance.Canonical

  @schema "sarah.model_program_artifact.v1"
  @runtime_compatibility 1
  @digest_regex ~r/\A[0-9a-f]{64}\z/
  @allowed_output_kinds ~w(proposal selection score)
  @allowed_activation_statuses ~w(shadow active)
  @maximum_document_bytes 65_536
  @maximum_prompt_blocks 32

  @required_top_level ~w(
    schema artifact_id artifact_digest signature compiler model decoding
    compatibility prompt_ir parameters datasets optimizer evaluator metrics
    provenance approval predecessor activation_status
  )

  @spec read(String.t()) :: {:ok, Artifact.t()} | {:error, term()}
  def read(contents)
      when is_binary(contents) and byte_size(contents) <= @maximum_document_bytes do
    read_document(contents, :admitted)
  end

  def read(_contents), do: {:error, :artifact_document_too_large_or_invalid}

  @spec read_candidate(String.t()) :: {:ok, Artifact.t()} | {:error, term()}
  def read_candidate(contents)
      when is_binary(contents) and byte_size(contents) <= @maximum_document_bytes do
    read_document(contents, :candidate)
  end

  def read_candidate(_contents), do: {:error, :artifact_document_too_large_or_invalid}

  defp read_document(contents, mode) do
    with {:ok, document} <- decode(contents),
         :ok <- validate(document, mode),
         digest <- digest(document),
         :ok <- digest_matches(document["artifact_digest"], digest) do
      {:ok,
       %Artifact{
         id: document["artifact_id"],
         signature_id: document["signature"]["id"],
         digest: digest,
         activation_status: document["activation_status"],
         predecessor: document["predecessor"],
         document: document
       }}
    end
  end

  @spec digest(map()) :: String.t()
  def digest(document) when is_map(document) do
    document
    |> Map.delete("artifact_digest")
    |> Canonical.digest!()
  end

  defp decode(contents) do
    case Jason.decode(contents) do
      {:ok, document} when is_map(document) -> {:ok, document}
      _invalid -> {:error, :artifact_json_invalid}
    end
  end

  defp validate(document, mode) do
    with :ok <- validate_top_level(document),
         :ok <- validate_signature(document["signature"]),
         :ok <- validate_compiler(document["compiler"]),
         :ok <- validate_model(document["model"], document["decoding"]),
         :ok <- validate_compatibility(document["compatibility"]),
         :ok <- validate_prompt(document["prompt_ir"], document["parameters"]),
         :ok <- validate_datasets(document["datasets"]),
         :ok <- validate_optimizer(document["optimizer"]),
         :ok <- validate_evaluation(document["evaluator"], document["metrics"]),
         :ok <- validate_provenance(document["provenance"]),
         :ok <- validate_approval(document["approval"], mode),
         :ok <- validate_activation(document["activation_status"], mode) do
      :ok
    end
  end

  defp validate_top_level(document) do
    cond do
      document["schema"] != @schema ->
        {:error, :incompatible_artifact_schema}

      Enum.any?(@required_top_level, &(not Map.has_key?(document, &1))) ->
        {:error, :artifact_field_missing}

      not non_empty?(document["artifact_id"]) ->
        {:error, :artifact_id_invalid}

      not valid_digest?(document["artifact_digest"]) ->
        {:error, :artifact_digest_invalid}

      document["predecessor"] != nil and not non_empty?(document["predecessor"]) ->
        {:error, :predecessor_invalid}

      true ->
        :ok
    end
  end

  defp validate_signature(%{
         "id" => id,
         "version" => version,
         "input_schema" => input_schema,
         "output_schema" => output_schema,
         "output_kind" => output_kind
       }) do
    cond do
      not non_empty?(id) or not positive_integer?(version) ->
        {:error, :program_signature_invalid}

      not bounded_schema?(input_schema) or not bounded_schema?(output_schema) ->
        {:error, :program_signature_invalid}

      output_kind not in @allowed_output_kinds ->
        {:error, :program_output_kind_forbidden}

      authority_fields?(output_schema) ->
        {:error, :program_output_authority_forbidden}

      true ->
        :ok
    end
  end

  defp validate_signature(_signature), do: {:error, :program_signature_invalid}

  defp validate_compiler(%{"id" => id, "version" => version, "source_digest" => digest}) do
    if non_empty?(id) and non_empty?(version) and valid_digest?(digest),
      do: :ok,
      else: {:error, :compiler_identity_invalid}
  end

  defp validate_compiler(_compiler), do: {:error, :compiler_identity_invalid}

  defp validate_model(%{"provider" => provider, "model" => model}, decoding)
       when is_map(decoding) do
    if non_empty?(provider) and non_empty?(model) and map_size(decoding) > 0,
      do: :ok,
      else: {:error, :model_or_decoding_identity_invalid}
  end

  defp validate_model(_model, _decoding), do: {:error, :model_or_decoding_identity_invalid}

  defp validate_compatibility(%{"runtime_min" => minimum, "runtime_max" => maximum}) do
    if positive_integer?(minimum) and positive_integer?(maximum) and
         minimum <= @runtime_compatibility and maximum >= @runtime_compatibility,
       do: :ok,
       else: {:error, :artifact_runtime_incompatible}
  end

  defp validate_compatibility(_compatibility), do: {:error, :artifact_runtime_incompatible}

  defp validate_prompt(%{"version" => version, "blocks" => blocks}, parameters)
       when is_list(blocks) and is_map(parameters) do
    cond do
      not positive_integer?(version) -> {:error, :prompt_ir_invalid}
      blocks == [] or length(blocks) > @maximum_prompt_blocks -> {:error, :prompt_ir_invalid}
      not Enum.all?(blocks, &valid_prompt_block?/1) -> {:error, :prompt_ir_invalid}
      byte_size(Jason.encode!(parameters)) > 16_384 -> {:error, :program_parameters_too_large}
      true -> :ok
    end
  end

  defp validate_prompt(_prompt, _parameters), do: {:error, :prompt_ir_invalid}

  defp validate_datasets(%{"train" => train, "validation" => validation, "holdout" => holdout}) do
    datasets = [train, validation, holdout]

    cond do
      not Enum.all?(datasets, &valid_dataset?/1) ->
        {:error, :dataset_manifest_invalid}

      length(Enum.uniq(Enum.map(datasets, & &1["id"]))) != 3 ->
        {:error, :true_holdout_not_independent}

      length(Enum.uniq(Enum.map(datasets, & &1["content_digest"]))) != 3 ->
        {:error, :true_holdout_not_independent}

      holdout["purpose"] != "true_holdout" ->
        {:error, :true_holdout_missing}

      true ->
        :ok
    end
  end

  defp validate_datasets(_datasets), do: {:error, :true_holdout_missing}

  defp validate_optimizer(%{"id" => id, "version" => version, "budget" => budget})
       when is_map(budget) do
    if non_empty?(id) and non_empty?(version) and valid_budget?(budget),
      do: :ok,
      else: {:error, :optimizer_budget_invalid}
  end

  defp validate_optimizer(_optimizer), do: {:error, :optimizer_budget_invalid}

  defp validate_evaluation(
         %{"id" => id, "version" => version, "digest" => digest},
         %{"baseline" => baseline, "candidate" => candidate, "uncertainty" => uncertainty}
       )
       when is_map(baseline) and is_map(candidate) and is_map(uncertainty) do
    if non_empty?(id) and non_empty?(version) and valid_digest?(digest) and
         map_size(baseline) > 0 and map_size(candidate) > 0 and map_size(uncertainty) > 0,
       do: :ok,
       else: {:error, :evaluation_evidence_invalid}
  end

  defp validate_evaluation(_evaluator, _metrics), do: {:error, :evaluation_evidence_invalid}

  defp validate_provenance(%{
         "source_revision" => revision,
         "compiled_at" => compiled_at,
         "compiled_by" => compiled_by,
         "receipt" => receipt
       }) do
    if non_empty?(revision) and valid_datetime?(compiled_at) and non_empty?(compiled_by) and
         is_map(receipt) and map_size(receipt) > 0,
       do: :ok,
       else: {:error, :artifact_provenance_invalid}
  end

  defp validate_provenance(_provenance), do: {:error, :artifact_provenance_invalid}

  defp validate_approval(
         %{
           "status" => "approved",
           "approved_by" => approved_by,
           "approved_at" => approved_at,
           "receipt" => receipt
         },
         :admitted
       ) do
    if non_empty?(approved_by) and valid_datetime?(approved_at) and is_map(receipt) and
         map_size(receipt) > 0,
       do: :ok,
       else: {:error, :artifact_approval_invalid}
  end

  defp validate_approval(%{"status" => "pending", "receipt" => receipt}, :candidate)
       when is_map(receipt),
       do: :ok

  defp validate_approval(_approval, _mode), do: {:error, :artifact_unapproved}

  defp validate_activation(status, :admitted) when status in @allowed_activation_statuses,
    do: :ok

  defp validate_activation("candidate", :candidate), do: :ok
  defp validate_activation(_status, _mode), do: {:error, :artifact_activation_status_invalid}

  defp valid_prompt_block?(%{"id" => id, "kind" => kind, "content" => content}),
    do:
      non_empty?(id) and kind in ~w(instruction examples output_contract) and non_empty?(content)

  defp valid_prompt_block?(_block), do: false

  defp valid_dataset?(%{
         "id" => id,
         "revision" => revision,
         "content_digest" => digest,
         "purpose" => purpose,
         "source_kind" => source_kind,
         "consent_receipt" => consent_receipt
       }) do
    source_valid? =
      (source_kind == "synthetic" and is_nil(consent_receipt)) or
        (source_kind == "consented_fixture" and is_map(consent_receipt) and
           map_size(consent_receipt) > 0)

    non_empty?(id) and non_empty?(revision) and valid_digest?(digest) and
      purpose in ~w(train validation true_holdout) and source_valid?
  end

  defp valid_dataset?(_dataset), do: false

  defp valid_budget?(%{
         "max_trials" => trials,
         "max_model_calls" => calls,
         "max_cost_usd" => cost
       }),
       do: positive_integer?(trials) and positive_integer?(calls) and is_number(cost) and cost > 0

  defp valid_budget?(_budget), do: false

  defp bounded_schema?(schema) when is_map(schema),
    do:
      schema["type"] == "object" and is_map(schema["properties"]) and is_list(schema["required"])

  defp bounded_schema?(_schema), do: false

  defp authority_fields?(output_schema) do
    forbidden =
      MapSet.new(~w(execute tool_call add_tool policy_update persona_update promote activate))

    output_schema["properties"]
    |> Map.keys()
    |> Enum.any?(&MapSet.member?(forbidden, &1))
  end

  defp digest_matches(digest, digest), do: :ok
  defp digest_matches(_stored, _calculated), do: {:error, :artifact_digest_mismatch}
  defp valid_digest?(value), do: is_binary(value) and Regex.match?(@digest_regex, value)
  defp positive_integer?(value), do: is_integer(value) and value > 0
  defp non_empty?(value), do: is_binary(value) and String.trim(value) != ""

  defp valid_datetime?(value) when is_binary(value) do
    match?({:ok, _datetime, 0}, DateTime.from_iso8601(value))
  end

  defp valid_datetime?(_value), do: false
end