Let the verifier take the name an operator actually has

183d69e53235 · AtlantisPleb · · parent d808cf179a62

Let the verifier take the name an operator actually has

The first acceptance criterion of issue #190: Verification.verify/1
accepts an owner/name path, resolves it through the same repository
mapping the serving path uses, and verifies the resolved storage key —
so the runbook's documented name stops producing wal_unreadable for a
repository whose log is intact. The report names both the requested
path and the resolved key, an unknown name is a typed
repository_not_found finding distinct from wal_unreadable, and a bare
storage key behaves exactly as before. EXIT-002's independence note
records the one DB step the resolution adds and why the WAL check
itself stays database-free.

The stale-bare-repo removal and allowed_repos reconciliation remain
open on #190.

Built by a Devin child through the openagents coder's delegate tool;
forge suite re-run before landing (the two failures on clean main —
exit_rehearsal_runbook and key_rotation #192 — pre-exist unrelated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

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

pushed
by user · WAL seq 335 · 2026-08-25T03:09:12.435324Z

Changed files

  • modified INVARIANTS.md
  • modified lib/openagents/forge/verification.ex
  • modified test/openagents/forge/independence_test.exs

Diff

3 files changed, +80 -25

INVARIANTS.md modified +13 -4

@@ -4237,10 +4237,19 @@ projection of that record. Whether the projection still matches is therefore a

4237 4237
question with an answer, and `OpenAgents.Forge.Verification` computes it from
4238 4238
the WAL and the repository alone.
4239 4239
4240
Independence here is structural, not a promise: a verifier that queried
4241
PostgreSQL would be asking the operator to confirm the operator. The proof reads
4242
the module's compiled import table and fails on a call into `OpenAgents.Repo`,
4243
Ecto, or Postgrex, so the property cannot decay through an added convenience.
4240
The verifier accepts either a `Repository.storage_key` or an `owner/name` path.
4241
An `owner/name` path is resolved to a storage key through the same repository
4242
mapping the serving path uses, and only the resolved key is checked against the
4243
WAL. The report names both the requested path and the resolved key so an operator
4244
sees the mapping.
4245
4246
Independence here is structural, not a promise: the WAL-and-repository check
4247
itself reaches no database, because a verifier that queried PostgreSQL for the
4248
log or the bare repository would be asking the operator to confirm the operator.
4249
The `owner/name` resolution does query the repository table, but it is a
4250
separate, exercised step. The proof reads the module's compiled import table and
4251
fails on a call into `OpenAgents.Repo`, Ecto, or Postgrex, so the property
4252
cannot decay through an added convenience.
4244 4253
4245 4254
Five disagreements are distinct findings, and each is exercised by breaking it.
4246 4255
An entry the store cannot produce is `entry_object_missing`. An entry whose
lib/openagents/forge/verification.ex modified +42 -21

@@ -53,7 +53,7 @@ defmodule OpenAgents.Forge.Verification do

53 53
  only an operator who serves something other than what was pushed.
54 54
  """
55 55
56
  alias OpenAgents.Forge.{Repos, WAL}
56
  alias OpenAgents.Forge.{ReceiptRepository, Repos, WAL}
57 57
58 58
  @internal_ref_prefix "refs/internal/"
59 59

@@ -66,6 +66,7 @@ defmodule OpenAgents.Forge.Verification do

66 66
  @typedoc "The verification outcome for one repository."
67 67
  @type report :: %{
68 68
          repo: String.t(),
69
          storage_key: String.t() | nil,
69 70
          entries: non_neg_integer(),
70 71
          findings: [finding()],
71 72
          head: anchor() | nil,

@@ -92,30 +93,50 @@ defmodule OpenAgents.Forge.Verification do

92 93
  sequence predate the chain and are not covered by it.
93 94
  """
94 95
  @spec verify(String.t(), keyword()) :: {:ok, report()} | {:error, report()}
95
  def verify(storage_key, opts \\ []) when is_binary(storage_key) and is_list(opts) do
96
    case WAL.read_index(storage_key) do
97
      {:ok, _generation, index} ->
98
        entries = WAL.entries(index)
99
100
        findings =
101
          sequence_findings(entries) ++
102
            entry_findings(storage_key, entries) ++
103
            ref_findings(storage_key, index) ++
104
            object_findings(storage_key, entries) ++
105
            reachability_findings(storage_key, index) ++
106
            chain_findings(entries) ++
107
            anchor_findings(entries, normalize_anchor(opts[:anchor]))
108
109
        report(storage_key, entries, findings)
110
111
      {:error, reason} ->
112
        report(storage_key, [], [finding("wal_unreadable", %{"reason" => inspect(reason)})])
96
  def verify(repo_or_path, opts \\ []) when is_binary(repo_or_path) and is_list(opts) do
97
    {repo, storage_key} = resolve_repo_or_path(repo_or_path)
98
99
    if is_nil(storage_key) do
100
      report(repo, nil, [], [finding("repository_not_found", %{"repo" => repo})])
101
    else
102
      case WAL.read_index(storage_key) do
103
        {:ok, _generation, index} ->
104
          entries = WAL.entries(index)
105
106
          findings =
107
            sequence_findings(entries) ++
108
              entry_findings(storage_key, entries) ++
109
              ref_findings(storage_key, index) ++
110
              object_findings(storage_key, entries) ++
111
              reachability_findings(storage_key, index) ++
112
              chain_findings(entries) ++
113
              anchor_findings(entries, normalize_anchor(opts[:anchor]))
114
115
          report(repo, storage_key, entries, findings)
116
117
        {:error, reason} ->
118
          report(repo, storage_key, [], [
119
            finding("wal_unreadable", %{"reason" => inspect(reason)})
120
          ])
121
      end
122
    end
123
  end
124
125
  defp resolve_repo_or_path(repo) when is_binary(repo) do
126
    if String.contains?(repo, "/") do
127
      case ReceiptRepository.resolve(repo) do
128
        %{storage_key: storage_key} -> {repo, storage_key}
129
        nil -> {repo, nil}
130
      end
131
    else
132
      {repo, repo}
113 133
    end
114 134
  end
115 135
116
  defp report(storage_key, entries, findings) do
136
  defp report(repo, storage_key, entries, findings) do
117 137
    report = %{
118
      repo: storage_key,
138
      repo: repo,
139
      storage_key: storage_key,
119 140
      entries: length(entries),
120 141
      findings: findings,
121 142
      head: head(entries),
test/openagents/forge/independence_test.exs modified +25

@@ -180,6 +180,31 @@ defmodule OpenAgents.Forge.IndependenceTest do

180 180
        refute match?("Postgrex" <> _rest, inspect(module))
181 181
      end
182 182
    end
183
184
    test "an owner/name path resolves to the same report as its storage key", context do
185
      seed_history!(context)
186
187
      assert {:ok, by_key} = Verification.verify(context.repo)
188
      assert {:ok, by_name} = Verification.verify("exit-owner/demo")
189
190
      assert by_name.repo == "exit-owner/demo"
191
      assert by_name.storage_key == context.repo
192
      assert by_key.entries == by_name.entries
193
      assert by_key.head == by_name.head
194
      assert by_key.findings == []
195
      assert by_name.findings == []
196
    end
197
198
    test "an unknown owner/name returns a repository_not_found finding", _context do
199
      {:error, result} = Verification.verify("exit-owner/no-such-repo")
200
201
      assert result.repo == "exit-owner/no-such-repo"
202
      assert result.storage_key == nil
203
      assert result.entries == 0
204
205
      assert [%{code: "repository_not_found", detail: %{"repo" => "exit-owner/no-such-repo"}}] =
206
               result.findings
207
    end
183 208
  end
184 209
185 210
  ## ── EXIT-005: each entry commits to the entry before it ────────────────

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