Add artifact_links table, Transparency context, and changelog tier fields

bae8efe6c7e8 · AtlantisPleb · · parent 7146389f12ff

Add artifact_links table, Transparency context, and changelog tier fields

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/changelog/entry.ex
  • added lib/openagents/transparency.ex
  • added lib/openagents/transparency/artifact_link.ex
  • added priv/repo/migrations/20260823150050_create_artifact_links.exs

Diff

4 files changed, +189 -1

lib/openagents/changelog/entry.ex modified +5 -1

@@ -35,6 +35,8 @@ defmodule OpenAgents.Changelog.Entry do

35 35
    field :build_receipt_id, :binary_id
36 36
    field :target_id, :binary_id
37 37
    field :deploy_receipt_id, :binary_id
38
    field :artifact_link_id, :binary_id
39
    field :transparency_tier, :string, default: nil
38 40
    timestamps(updated_at: false)
39 41
  end
40 42

@@ -58,7 +60,9 @@ defmodule OpenAgents.Changelog.Entry do

58 60
      :push_receipt_id,
59 61
      :build_receipt_id,
60 62
      :target_id,
61
      :deploy_receipt_id
63
      :deploy_receipt_id,
64
      :artifact_link_id,
65
      :transparency_tier
62 66
    ])
63 67
    |> validate_required([:repo, :sha, :summary, :category, :source, :entry_at])
64 68
    |> validate_format(:sha, ~r/^[0-9a-f]{7,40}$/)
lib/openagents/transparency.ex added +96

@@ -0,0 +1,96 @@

1
defmodule OpenAgents.Transparency do
2
  @moduledoc """
3
  Context helpers for artifact transparency tiers.
4
5
  Tiers follow the same `dark/pulse/ledger/glass` naming used by the forge
6
  visibility levels:
7
8
    * `dark`   — nothing public
9
    * `pulse`  — metadata only
10
    * `ledger` — content and metadata
11
    * `glass`  — full access
12
  """
13
14
  import Ecto.Changeset
15
16
  alias OpenAgents.Transparency.ArtifactLink
17
18
  @tier_rank %{dark: 0, pulse: 1, ledger: 2, glass: 3}
19
  @tier_strings %{
20
    "dark" => :dark,
21
    "pulse" => :pulse,
22
    "ledger" => :ledger,
23
    "glass" => :glass
24
  }
25
26
  @capability_min %{
27
    metadata: :pulse,
28
    content: :ledger,
29
    full: :glass
30
  }
31
32
  @doc "All tier atoms, from least to most transparent."
33
  def tier_atoms, do: [:dark, :pulse, :ledger, :glass]
34
35
  @doc """
36
  The effective tier for `tier_or_link` given `viewer`.
37
38
  `viewer` may be a tier atom/string, a map with a `:tier` field, or `nil`.
39
  A revoked `ArtifactLink` always resolves to `:dark`.
40
  """
41
  def effective_tier(%ArtifactLink{revoked_at: nil} = link, viewer) do
42
    clamp(tier_atom(link.tier), viewer)
43
  end
44
45
  def effective_tier(%ArtifactLink{}, _viewer), do: :dark
46
  def effective_tier(tier, viewer), do: clamp(tier_atom(tier), viewer)
47
48
  @doc """
49
  Whether `tier_or_link` admits `capability` for `viewer`.
50
51
  Capabilities are `:metadata`, `:content`, and `:full`.
52
  """
53
  def allows?(tier, capability, viewer) do
54
    effective = effective_tier(tier, viewer)
55
    required = Map.get(@capability_min, capability, :dark)
56
    @tier_rank[effective] >= @tier_rank[required]
57
  end
58
59
  @doc """
60
  Marks `artifact_link` as revoked by `revoked_by_id` for `reason`.
61
62
  Returns a changeset with `revoked_at` and `revocation_tombstone` set.
63
  """
64
  def revoke(artifact_link, reason, revoked_by_id) do
65
    now = DateTime.utc_now()
66
67
    change(artifact_link)
68
    |> put_change(:revoked_at, now)
69
    |> put_change(:revocation_tombstone, %{
70
      reason: reason,
71
      revoked_by_id: revoked_by_id,
72
      revoked_at: DateTime.to_iso8601(now)
73
    })
74
  end
75
76
  defp tier_atom(tier) when is_atom(tier) do
77
    Map.get(@tier_strings, to_string(tier), :dark)
78
  end
79
80
  defp tier_atom(tier) when is_binary(tier) do
81
    Map.get(@tier_strings, tier, :dark)
82
  end
83
84
  defp tier_atom(_), do: :dark
85
86
  defp viewer_tier(%{tier: tier}), do: tier_atom(tier)
87
  defp viewer_tier(tier) when is_atom(tier) or is_binary(tier), do: tier_atom(tier)
88
  defp viewer_tier(_), do: nil
89
90
  defp clamp(tier, viewer) do
91
    case viewer_tier(viewer) do
92
      nil -> tier
93
      v -> if @tier_rank[v] < @tier_rank[tier], do: v, else: tier
94
    end
95
  end
96
end

This page updates live while a promote is in flight · changelog