priv/repo/migrations/20260820032524_add_turn_provenance_immutability.exs

58e6347eeb72 · 10 KB

defmodule OpenAgents.Repo.Migrations.AddTurnProvenanceImmutability do
  use Ecto.Migration

  def up do
    execute("""
    CREATE OR REPLACE FUNCTION prevent_turn_receipt_identity_update()
    RETURNS trigger AS $$
    BEGIN
      IF ROW(
        OLD.turn_id,
        OLD.schema_version,
        OLD.model_id,
        OLD.persona_id,
        OLD.persona_digest,
        OLD.role_id,
        OLD.role_digest,
        OLD.instruction_digest,
        OLD.input_digest,
        OLD.input_message_count,
        OLD.input_bytes,
        OLD.tool_catalog_digest,
        OLD.blueprint_revision,
        OLD.blueprint_digest,
        OLD.program_artifact_id,
        OLD.program_artifact_digest,
        OLD.memory_snapshot_ref,
        OLD.provider_started_at
      ) IS DISTINCT FROM ROW(
        NEW.turn_id,
        NEW.schema_version,
        NEW.model_id,
        NEW.persona_id,
        NEW.persona_digest,
        NEW.role_id,
        NEW.role_digest,
        NEW.instruction_digest,
        NEW.input_digest,
        NEW.input_message_count,
        NEW.input_bytes,
        NEW.tool_catalog_digest,
        NEW.blueprint_revision,
        NEW.blueprint_digest,
        NEW.program_artifact_id,
        NEW.program_artifact_digest,
        NEW.memory_snapshot_ref,
        NEW.provider_started_at
      ) THEN
        RAISE EXCEPTION 'turn receipt identity is immutable';
      END IF;

      IF OLD.status <> 'captured' AND ROW(
        OLD.status,
        OLD.used_source_refs,
        OLD.used_tool_step_refs,
        OLD.used_preferences,
        OLD.used_experiences,
        OLD.used_memory_evidence,
        OLD.usage,
        OLD.provider_completed_at
      ) IS DISTINCT FROM ROW(
        NEW.status,
        NEW.used_source_refs,
        NEW.used_tool_step_refs,
        NEW.used_preferences,
        NEW.used_experiences,
        NEW.used_memory_evidence,
        NEW.usage,
        NEW.provider_completed_at
      ) THEN
        RAISE EXCEPTION 'terminal turn receipt is immutable';
      END IF;

      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("DROP TRIGGER IF EXISTS turn_receipts_prevent_identity_update ON turn_receipts")

    execute("""
    CREATE TRIGGER turn_receipts_prevent_identity_update
    BEFORE UPDATE ON turn_receipts
    FOR EACH ROW
    EXECUTE FUNCTION prevent_turn_receipt_identity_update()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION protect_turn_role_selection()
    RETURNS trigger AS $$
    BEGIN
      IF OLD.role_selection IS DISTINCT FROM NEW.role_selection THEN
        RAISE EXCEPTION 'turn role selection is immutable';
      END IF;
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("DROP TRIGGER IF EXISTS protect_turn_role_selection_trigger ON turn_receipts")

    execute("""
    CREATE TRIGGER protect_turn_role_selection_trigger
    BEFORE UPDATE ON turn_receipts
    FOR EACH ROW EXECUTE FUNCTION protect_turn_role_selection()
    """)

    execute("DROP TRIGGER IF EXISTS protect_voice_role_selection_trigger ON voice_sessions")

    execute("""
    CREATE TRIGGER protect_voice_role_selection_trigger
    BEFORE UPDATE ON voice_sessions
    FOR EACH ROW EXECUTE FUNCTION protect_turn_role_selection()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION prevent_turn_receipt_profile_memory_snapshot_update()
    RETURNS trigger AS $$
    BEGIN
      IF OLD.profile_memory_snapshot_ref IS DISTINCT FROM NEW.profile_memory_snapshot_ref THEN
        RAISE EXCEPTION 'turn receipt profile memory snapshot is immutable';
      END IF;
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute(
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_profile_memory_snapshot_update ON turn_receipts"
    )

    execute("""
    CREATE TRIGGER turn_receipts_prevent_profile_memory_snapshot_update
    BEFORE UPDATE ON turn_receipts
    FOR EACH ROW EXECUTE FUNCTION prevent_turn_receipt_profile_memory_snapshot_update()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION protect_turn_program_capture()
    RETURNS trigger AS $$
    BEGIN
      IF OLD.program_artifact_receipt IS DISTINCT FROM NEW.program_artifact_receipt THEN
        RAISE EXCEPTION 'turn program capture is immutable';
      END IF;
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("DROP TRIGGER IF EXISTS protect_turn_program_capture_trigger ON turn_receipts")

    execute("""
    CREATE TRIGGER protect_turn_program_capture_trigger
    BEFORE UPDATE ON turn_receipts
    FOR EACH ROW EXECUTE FUNCTION protect_turn_program_capture()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION prevent_terminal_memory_evidence_update()
    RETURNS trigger AS $$
    BEGIN
      IF OLD.status <> 'captured' AND
         OLD.used_memory_evidence IS DISTINCT FROM NEW.used_memory_evidence THEN
        RAISE EXCEPTION 'terminal memory evidence is immutable';
      END IF;

      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute(
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_terminal_memory_evidence_update ON turn_receipts"
    )

    execute("""
    CREATE TRIGGER turn_receipts_prevent_terminal_memory_evidence_update
    BEFORE UPDATE ON turn_receipts
    FOR EACH ROW
    EXECUTE FUNCTION prevent_terminal_memory_evidence_update()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION prevent_provider_step_rewrite()
    RETURNS trigger AS $$
    BEGIN
      IF ROW(
        OLD.turn_receipt_id,
        OLD.sequence,
        OLD.provider_id,
        OLD.model_id,
        OLD.started_at
      ) IS DISTINCT FROM ROW(
        NEW.turn_receipt_id,
        NEW.sequence,
        NEW.provider_id,
        NEW.model_id,
        NEW.started_at
      ) THEN
        RAISE EXCEPTION 'provider step identity is immutable';
      END IF;

      IF OLD.status <> 'started' AND ROW(
        OLD.status,
        OLD.provider_response_id,
        OLD.usage,
        OLD.error_code,
        OLD.completed_at
      ) IS DISTINCT FROM ROW(
        NEW.status,
        NEW.provider_response_id,
        NEW.usage,
        NEW.error_code,
        NEW.completed_at
      ) THEN
        RAISE EXCEPTION 'terminal provider step is immutable';
      END IF;

      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("DROP TRIGGER IF EXISTS turn_provider_steps_prevent_rewrite ON turn_provider_steps")

    execute("""
    CREATE TRIGGER turn_provider_steps_prevent_rewrite
    BEFORE UPDATE ON turn_provider_steps
    FOR EACH ROW
    EXECUTE FUNCTION prevent_provider_step_rewrite()
    """)

    execute("""
    CREATE OR REPLACE FUNCTION enforce_turn_tool_step_transition()
    RETURNS trigger AS $$
    BEGIN
      IF ROW(
        OLD.turn_id, OLD.turn_receipt_id, OLD.sequence, OLD.provider_call_id,
        OLD.provider_item_id, OLD.provider_response_id, OLD.tool_name,
        OLD.tool_version, OLD.module_id, OLD.module_artifact_digest,
        OLD.executor_implementation_digest, OLD.routing_receipt_id,
        OLD.side_effect_class, OLD.invocation_key, OLD.attribution_policy_id,
        OLD.attribution_policy_version, OLD.attribution_policy_digest,
        OLD.billable, OLD.billable_attribution_key, OLD.cost_units,
        OLD.catalog_digest, OLD.raw_arguments, OLD.argument_digest, OLD.requested_at
      ) IS DISTINCT FROM ROW(
        NEW.turn_id, NEW.turn_receipt_id, NEW.sequence, NEW.provider_call_id,
        NEW.provider_item_id, NEW.provider_response_id, NEW.tool_name,
        NEW.tool_version, NEW.module_id, NEW.module_artifact_digest,
        NEW.executor_implementation_digest, NEW.routing_receipt_id,
        NEW.side_effect_class, NEW.invocation_key, NEW.attribution_policy_id,
        NEW.attribution_policy_version, NEW.attribution_policy_digest,
        NEW.billable, NEW.billable_attribution_key, NEW.cost_units,
        NEW.catalog_digest, NEW.raw_arguments, NEW.argument_digest, NEW.requested_at
      ) THEN
        RAISE EXCEPTION 'tool step identity is immutable';
      END IF;

      IF OLD.status = 'requested' AND NEW.status NOT IN (
        'requested', 'running', 'succeeded', 'failed', 'refused',
        'cancelled', 'unavailable', 'interrupted'
      ) THEN
        RAISE EXCEPTION 'invalid requested tool step transition';
      END IF;

      IF OLD.status = 'running' AND NEW.status NOT IN (
        'running', 'succeeded', 'failed', 'refused', 'cancelled',
        'unavailable', 'interrupted'
      ) THEN
        RAISE EXCEPTION 'invalid running tool step transition';
      END IF;

      IF OLD.status NOT IN ('requested', 'running') AND ROW(
        OLD.status, OLD.outcome_digest, OLD.outcome_receipt_ref, OLD.usage,
        OLD.result, OLD.error,
        OLD.executor_id, OLD.executor_disclosure, OLD.target_receipt_refs,
        OLD.attribution_refs, OLD.started_at, OLD.completed_at
      ) IS DISTINCT FROM ROW(
        NEW.status, NEW.outcome_digest, NEW.outcome_receipt_ref, NEW.usage,
        NEW.result, NEW.error,
        NEW.executor_id, NEW.executor_disclosure, NEW.target_receipt_refs,
        NEW.attribution_refs, NEW.started_at, NEW.completed_at
      ) THEN
        RAISE EXCEPTION 'terminal tool step is immutable';
      END IF;

      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    """)

    execute("DROP TRIGGER IF EXISTS turn_tool_steps_enforce_transition ON turn_tool_steps")

    execute("""
    CREATE TRIGGER turn_tool_steps_enforce_transition
    BEFORE UPDATE ON turn_tool_steps
    FOR EACH ROW
    EXECUTE FUNCTION enforce_turn_tool_step_transition()
    """)
  end

  def down do
    execute("DROP TRIGGER IF EXISTS turn_tool_steps_enforce_transition ON turn_tool_steps")
    execute("DROP FUNCTION IF EXISTS enforce_turn_tool_step_transition()")

    execute("DROP TRIGGER IF EXISTS turn_provider_steps_prevent_rewrite ON turn_provider_steps")
    execute("DROP FUNCTION IF EXISTS prevent_provider_step_rewrite()")

    execute(
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_terminal_memory_evidence_update ON turn_receipts"
    )

    execute("DROP FUNCTION IF EXISTS prevent_terminal_memory_evidence_update()")

    execute("DROP TRIGGER IF EXISTS protect_turn_program_capture_trigger ON turn_receipts")
    execute("DROP FUNCTION IF EXISTS protect_turn_program_capture()")

    execute(
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_profile_memory_snapshot_update ON turn_receipts"
    )

    execute("DROP FUNCTION IF EXISTS prevent_turn_receipt_profile_memory_snapshot_update()")

    execute("DROP TRIGGER IF EXISTS protect_voice_role_selection_trigger ON voice_sessions")
    execute("DROP TRIGGER IF EXISTS protect_turn_role_selection_trigger ON turn_receipts")
    execute("DROP FUNCTION IF EXISTS protect_turn_role_selection()")

    execute("DROP TRIGGER IF EXISTS turn_receipts_prevent_identity_update ON turn_receipts")
    execute("DROP FUNCTION IF EXISTS prevent_turn_receipt_identity_update()")
  end
end