Report how far the running forge is from the revision its proofs ran on

0aa6cde7da7e · AtlantisPleb · · parent b2e5ee0634de

Report how far the running forge is from the revision its proofs ran on

exit surfaces proven by tests on `main` were absent from the live forge: the
account export 404'd, the pushes route 404'd, `OpenAgents.Forge.Independence`
did not load, the WAL chain covered 0 of 275 entries. Every tip-shaped check
stayed green, because an invariant reads the compiled modules of the node
running the test suite and not the ones running the forge. A person found it by
rehearsing six surfaces by hand. Nothing reported it.

`OpenAgents.Forge.Independence` now publishes its own distance from the
revision its proofs ran against. `independence.deployment.behind` is the number
of commits on the head this node serves that the running revision does not
carry, and `/status` renders it in words. It is derived the way every other
claim in that module is: the running revision is the one `/api/status` already
publishes for this node, the proven revision is `refs/heads/main` in the bare
projection the forge serves of its own repository, and the distance is
`git rev-list --count` over objects already on disk. No outside service is
asked, and the forge hosting itself is what makes that possible. Production
reads 17 behind as this lands.

EXIT-006's proof was not weakened to make this fit. It turns red when a commit
sha reaches this projection — adding the forge's repository name was one of the
six mutations it was confirmed against — and that rule is untouched. A distance
is a count and fits under it; the two revisions it lies between do not, and
they are not published. Three further mutations were confirmed and reverted:
publishing the running revision beside its distance, which the vocabulary
assertion and both STATUS-001 key-set assertions catch; hardcoding the distance
to zero, which the seeded-repository counts catch and the compiled-import-table
read catches independently; and dropping the section, which STATUS-001's
stale-key assertion catches.

The claim is narrow on purpose. It is not "how far behind `main` this forge
is": nothing on this node can see a `main` it declines to serve. A forge that
will not serve its own repository, an empty bare projection, and a release
built from a revision this forge never accepted all report `known: false` and
no distance at all, which is the same withholding EXIT-005 and EXIT-006 already
decline to detect. Being behind is also not an axis of `degraded?`. A node one
commit behind is not less independent, and folding ordinary deploy lag into the
independence verdict would make that verdict mean nothing on the day it
mattered; the distance is published beside the verdict, as its margin of error.
`deployment/1` is public with the revision as a parameter for the reason
`degraded?/3` is: the node running the proof reports a build revision that is
not a commit at all, so the projection alone could only ever exercise the
branch that withholds.

STATUS-001 and EXIT-006 record the decision, and the published key set moved
with the claim as STATUS-001 requires.

Closes #246.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnhfrafYx5ZGaMbzZEJQ2d
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes
#246

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 393 · 2026-08-25T15:02:39.328900Z

Changed files

  • modified INVARIANTS.md
  • modified docs/forge-exit-rehearsals.md
  • modified docs/forge-operator-independence.md
  • modified lib/openagents/forge/independence.ex
  • modified lib/openagents/network_status.ex
  • modified lib/openagents_web/live/network_status_live.ex
  • modified test/openagents/forge/independence_disclosure_test.exs
  • modified test/openagents/network_status_test.exs

Diff

8 files changed, +383 -9

INVARIANTS.md modified +60 -5

@@ -3849,6 +3849,17 @@ instance data: it is where a reader goes to hold the forge to `EXIT-005`, and a

3849 3849
disclosure that named a gap without naming the surface that closes it would be
3850 3850
asking the reader to go looking.
3851 3851
3852
It carries one more decision (issue #246). `independence.deployment` publishes
3853
how far the code assembling this projection is from the head of the ref its
3854
proofs ran against: `proven_ref` (`refs/heads/main`, a fixed ref name and not
3855
instance data), `known`, and `behind`. A count of commits is a count, so the
3856
content rule is unchanged and the shas that count lies between stay off this
3857
page — `EXIT-006`'s proof turns red for one, and this decision does not relax
3858
that. It belongs in the same bounded projection for the reason the disclosure
3859
does: a status page assembled by code 57 commits behind the ledger reports
3860
health for surfaces the reader cannot reach (#187), and nothing else on this
3861
page can tell them so.
3862
3852 3863
What the projection carries beside counts is the bounded public SCV activity
3853 3864
band (`scvs`: a derived public id, a label, a status, a weight, and one
3854 3865
bounded activity line), the forge deploy lane (short shas, statuses, timings,

@@ -4938,12 +4949,49 @@ published key set moved with the claim, as `STATUS-001` requires:

4938 4949
`export_recipient_encryption`, and `operator_reads_source` was added. That is
4939 4950
the decision being asked for, not an accident.
4940 4951
4952
Amended 2026-08-25 (issue #246). The disclosure publishes its own distance from
4953
the revision its proofs ran against. Every claim above is derived from the
4954
running node, and the running node's code can be older than the ledger those
4955
claims answer to: #187 found the forge serving a revision 57 commits behind
4956
`main`, where the export route, the `EXIT-005` chain, and this module itself did
4957
not exist, while every proof of them stayed green. An invariant that is proven
4958
and not deployed reports nothing, which is the outcome this contract exists to
4959
prevent, reached by a second route. So `independence.deployment` publishes the
4960
number of commits on the head this node serves that the running revision does
4961
not carry, and `/status` renders it in words.
4962
4963
It is a distance and nothing else. Both revisions it lies between are commit
4964
shas, and adding one to this projection was among the mutations below; the
4965
proof stays red for them, and this amendment does not touch that rule. The
4966
distance is derived the way everything else here is: the running revision is
4967
the one `/api/status` already publishes for this node, the proven revision is
4968
the head of `refs/heads/main` in the bare projection this node serves, and the
4969
count is `git rev-list` over objects already on disk. `RELEASE-004` binds the
4970
proof matrix to the exact candidate sha and `.githooks/pre-push` refuses a push
4971
without it, which is what makes that head the newest proven revision.
4972
4973
The claim is narrow on purpose. It is not "how far behind `main` this forge
4974
is" — nothing on this node can see a `main` it declines to serve. A forge that
4975
will not serve its own repository, a node whose bare projection is empty, and a
4976
release built from a revision this forge never accepted all report `known:
4977
false` and no distance, which is the same withholding `EXIT-005` and `EXIT-006`
4978
already decline to detect. The distance is also not an axis of `degraded?`: a
4979
node one commit behind is not less independent, and folding ordinary deploy lag
4980
into the independence verdict would make that verdict mean nothing on the day
4981
it mattered. It is published beside the verdict, as the verdict's margin of
4982
error. The published key set moved with the claim, as `STATUS-001` requires:
4983
`independence.deployment.proven_ref`, `.known`, and `.behind` were added, and
4984
`deployment/1` is public with the revision as a parameter for the same reason
4985
`degraded?/3` is — the node running the proof reports a build revision that is
4986
not a commit at all, so the projection alone could only ever exercise the
4987
branch that withholds.
4988
4941 4989
`STATUS-001`'s rule holds here: the section carries counts, booleans, family
4942
names, issue numbers, and one document path, and the proof asserts that every
4943
string it publishes is a ledger family name or fixed vocabulary — a repository
4944
path, an account id, a node name, or a commit sha reaching it turns the proof
4945
red. The whole section degrades to `nil` like every other gather, so a node
4946
that cannot assemble it renders the page without it rather than failing.
4990
names, issue numbers, one ref name, and one document path, and the proof asserts
4991
that every string it publishes is a ledger family name or fixed vocabulary — a
4992
repository path, an account id, a node name, or a commit sha reaching it turns
4993
the proof red. The whole section degrades to `nil` like every other gather, so a
4994
node that cannot assemble it renders the page without it rather than failing.
4947 4995
4948 4996
Six mutations were confirmed to fail the proof and reverted: publishing an
4949 4997
empty gap list while the ledger records gaps; making `degraded` constant;

@@ -4963,6 +5011,13 @@ reverted to a literal `false`, which comparing the two values cannot catch —

4963 5011
`false` is the correct answer today — so the proof reads the compiled import
4964 5012
table the way `export_recipient_encryption` already does.
4965 5013
5014
Three more were confirmed for the deployment section and reverted: publishing
5015
the running revision beside its distance, which the vocabulary assertion and
5016
both `STATUS-001` key-set assertions catch; hardcoding the distance to zero,
5017
which the seeded-repository counts catch and the compiled-import-table read
5018
catches independently of them; and dropping the section from the projection,
5019
which `STATUS-001`'s stale-key assertion catches.
5020
4966 5021
Evidence: `OpenAgents.Forge.Independence`, `OpenAgents.Forge.AtRest`,
4967 5022
`OpenAgents.NetworkStatus`,
4968 5023
`OpenAgentsWeb.NetworkStatusLive`, `OpenAgents.DataRights.Age`,
docs/forge-exit-rehearsals.md modified +12

@@ -67,6 +67,18 @@ removes today's gap rather than the next one. #246 carries that, and the

67 67
obvious home for it is not free: `EXIT-006`'s proof turns red when a commit
68 68
sha reaches the disclosure.
69 69
70
**What #246 added.** The disclosure now publishes its own distance from the
71
revision its proofs ran against. `independence.deployment.behind` on `/status`
72
and `GET /api/status` is the number of commits on the head this node serves
73
that the running revision does not carry, so the next 57-commit gap is a
74
number on a public page rather than a rehearsal waiting to be performed. The
75
proof stayed as it was: a distance is a count, and neither revision it lies
76
between reaches the projection. What it still does not report is a forge that
77
withholds its own repository — that node reports `known: false` and no
78
distance, the same withholding `EXIT-005` and `EXIT-006` already decline to
79
detect — and it does not act on the gap it reports. Nothing raises an incident
80
when the distance grows; a reader or a check has to look.
81
70 82
## 1. Restore a repository and its bounded work history
71 83
72 84
**Proves:** an account's source and its work records survive on a machine that
docs/forge-operator-independence.md modified +16

@@ -327,6 +327,22 @@ rather than blurred.

327 327
A forge that records its limits in a document and reports itself healthy on its
328 328
status page has hidden them.
329 329
330
The disclosure also publishes its own distance from the revision its proofs ran
331
against. Every claim in it is derived from the running node, and the running
332
node's code can be older than this ledger: in August 2026 the forge served a
333
revision 57 commits behind `main`, where the account export, the WAL chain, and
334
the disclosure itself did not exist, while every proof of them stayed green
335
(#187). `independence.deployment.behind` is the number of commits on the head
336
this node serves that the running revision does not carry, so a reader who
337
checks `/status` sees the gap without performing a rehearsal to find it (#246).
338
339
Its bound is the same withholding the rest of this document names. The distance
340
is measured against the head this node serves, so a forge that will not serve
341
its own repository, a node whose bare projection is empty, and a release built
342
from a revision this forge never accepted all report `known: false` and no
343
distance at all. Being behind is reported beside the independence verdict, not
344
inside it: a node one commit behind is not less independent.
345
330 346
## Rehearsals
331 347
332 348
`docs/forge-exit-rehearsals.md` defines six rehearsals — restore, receipt
lib/openagents/forge/independence.ex modified +109 -3

@@ -48,15 +48,34 @@ defmodule OpenAgents.Forge.Independence do

48 48
  `degraded?` is true while any of the three falls short, so the page does not
49 49
  need a human to decide when to say so. It is expected to be true today.
50 50
51
  A fourth section says how much any of that is worth. Every claim above is
52
  derived from the running node, and the running node's code can be older than
53
  the ledger those claims answer to: #187 found the forge serving a revision 57
54
  commits behind `main`, where the export route, the chain, and this module
55
  itself did not exist, while every proof of them stayed green on `main`. A
56
  disclosure that cannot say how far its own code is from the code its proofs
57
  ran against is asking to be trusted on exactly the point it exists to remove,
58
  so `deployment` publishes that distance: the number of commits on the head
59
  this node serves that the running revision does not carry (#246).
60
61
  It is a distance and nothing else. Publishing the two revisions it lies
62
  between would put a commit sha on an anonymous surface, which `STATUS-001`
63
  keeps content-free and this section's own proof turns red for. It is also not
64
  an axis of `degraded?`: a node one commit behind is not less independent, and
65
  folding ordinary deploy lag into the independence verdict would make that
66
  verdict mean nothing on the day it mattered.
67
51 68
  Nothing here carries content: family names, counts, booleans, issue numbers,
52
  and a document path. It answers with the same shape when the ledger is
53
  unreadable, so `STATUS-001`'s rule that the page renders during an incident
54
  still holds.
69
  a ref name, and a document path. It answers with the same shape when the
70
  ledger is unreadable, so `STATUS-001`'s rule that the page renders during an
71
  incident still holds.
55 72
  """
56 73
57 74
  alias OpenAgents.DataRights.ExportInventory
58 75
  alias OpenAgents.Forge.Anchor
59 76
  alias OpenAgents.Forge.AtRest
77
  alias OpenAgents.Forge.Pushes
78
  alias OpenAgents.Forge.Repos
60 79
61 80
  @schema "openagents.forge_independence.v1"
62 81

@@ -68,6 +87,13 @@ defmodule OpenAgents.Forge.Independence do

68 87
  @export_controller OpenAgentsWeb.DataController
69 88
  @account_export OpenAgents.DataRights.AccountExport
70 89
90
  # The ref a candidate is proven against before it is pushed: `RELEASE-004`
91
  # binds the full local matrix to the exact candidate sha and
92
  # `.githooks/pre-push` refuses a push without it, so the head of this ref is
93
  # the newest revision the ledger's proofs have run against.
94
  @proven_ref "refs/heads/main"
95
  @sha ~r/\A[0-9a-f]{40}\z/
96
71 97
  @doc "The disclosure, in the shape `/api/status` publishes it."
72 98
  @spec projection() :: map()
73 99
  def projection do

@@ -82,6 +108,7 @@ defmodule OpenAgents.Forge.Independence do

82 108
      "export" => export,
83 109
      "verification" => verification,
84 110
      "private_data" => private_data,
111
      "deployment" => deployment_section(),
85 112
      "document" => @document
86 113
    }
87 114
  end

@@ -200,6 +227,85 @@ defmodule OpenAgents.Forge.Independence do

200 227
    end
201 228
  end
202 229
230
  # How far the code answering all of the above is from the code the ledger's
231
  # proofs ran against. The forge hosts its own repository, so no outside
232
  # service is consulted: the running revision is the one this node reports as
233
  # deployed, the proven revision is the head of `refs/heads/main` in the bare
234
  # projection this node serves, and the distance between them is a `git`
235
  # count over objects already on disk.
236
  #
237
  # The claim is deliberately narrow. It is not "how far behind `main` this
238
  # forge is" — nothing here can see a `main` this node declines to serve. It
239
  # is "how many commits on the head this node serves the running revision does
240
  # not carry", which is exactly what the count establishes. A forge that will
241
  # not serve its own repository, a node whose bare projection is empty, and a
242
  # release built from a revision this forge never accepted all report `known:
243
  # false` and no distance, which is the same withholding `EXIT-005` and
244
  # `EXIT-006` already decline to detect.
245
  defp deployment_section, do: deployment(safely(&running_revision/0))
246
247
  @doc """
248
  The deployment section for a given running revision.
249
250
  Public with the revision as a parameter for the same reason `degraded?/3` is
251
  public: the node that runs this proof reports a build revision that is not a
252
  commit at all, so the projection alone can only ever exercise the branch that
253
  withholds. A proof that never sees a real distance cannot fail on the day one
254
  stops being computed.
255
  """
256
  @spec deployment(String.t() | nil) :: map()
257
  def deployment(revision) do
258
    behind = safely(fn -> commits_behind(revision) end)
259
260
    %{
261
      "proven_ref" => @proven_ref,
262
      "known" => is_integer(behind),
263
      "behind" => behind
264
    }
265
  end
266
267
  defp commits_behind(running) do
268
    with repo when is_binary(repo) <- List.first(Repos.allowed_repos()),
269
         storage_key = Pushes.mirror_storage_key(repo),
270
         true <- sha?(running),
271
         head when is_binary(head) <- Map.get(Repos.refs(storage_key), @proven_ref),
272
         true <- sha?(head) do
273
      count_between(storage_key, running, head)
274
    else
275
      _unknown -> nil
276
    end
277
  end
278
279
  # The same revision `/api/status` publishes for this node, so the distance
280
  # and the revision it is measured from can never describe different code.
281
  defp running_revision do
282
    OpenAgents.Forge.DeploymentNode.health()["revision"]
283
  end
284
285
  defp sha?(value), do: is_binary(value) and Regex.match?(@sha, value)
286
287
  # `running..head` counts commits reachable from the served head and not from
288
  # the running revision, which stays the right number when the running
289
  # revision is not an ancestor of it. A revision the bare projection has never
290
  # heard of fails the read, and a failed read claims nothing.
291
  defp count_between(storage_key, running, head) do
292
    case Repos.git(Repos.bare_path(storage_key), [
293
           "rev-list",
294
           "--count",
295
           "--end-of-options",
296
           running <> ".." <> head
297
         ]) do
298
      {output, 0} ->
299
        case Integer.parse(String.trim(output)) do
300
          {behind, ""} -> behind
301
          _unparsed -> nil
302
        end
303
304
      _unreadable ->
305
        nil
306
    end
307
  end
308
203 309
  defp operator_section do
204 310
    %{
205 311
      "model" => "single_operator",
lib/openagents/network_status.ex modified +4 -1

@@ -29,7 +29,10 @@ defmodule OpenAgents.NetworkStatus do

29 29
  projection because a single operator who can read and rewrite everything is a
30 30
  fact about the service's condition, not a footnote in a document. It carries
31 31
  counts, booleans, family names, and issue numbers only, and degrades to `nil`
32
  like every other gather.
32
  like every other gather. Its `deployment` section publishes how far the code
33
  assembling this projection is from the head of the ref its proofs ran
34
  against, because a disclosure compiled 57 commits ago answers for code nobody
35
  is running (#187, #246).
33 36
  """
34 37
35 38
  @schema "openagents.network_status.v1"
lib/openagents_web/live/network_status_live.ex modified +18

@@ -291,6 +291,21 @@ defmodule OpenAgentsWeb.NetworkStatusLive do

291 291
  defp independence_text(%{"degraded" => true}), do: "degraded"
292 292
  defp independence_text(_disclosure), do: "no disclosed gap"
293 293
294
  # A proven invariant that is not deployed reports nothing, so the distance
295
  # between the two is worth a line of its own (#246). It says what it cannot
296
  # tell you as plainly as what it can.
297
  defp deployment_text(%{"known" => true, "behind" => 0, "proven_ref" => ref}),
298
    do: "running the head of #{ref}"
299
300
  defp deployment_text(%{"known" => true, "behind" => behind, "proven_ref" => ref})
301
       when is_integer(behind),
302
       do:
303
         "#{behind} #{if behind == 1, do: "commit", else: "commits"} behind #{ref}; " <>
304
           "an invariant proven there is not proven here"
305
306
  defp deployment_text(%{"proven_ref" => ref}),
307
    do: "distance from #{ref} unknown; this node serves no head it can measure against"
308
294 309
  defp gap_text(%{"family" => family, "status" => status, "issue" => issue})
295 310
       when is_integer(issue),
296 311
       do: "#{family} · #{status} · ##{issue}"

@@ -604,6 +619,9 @@ defmodule OpenAgentsWeb.NetworkStatusLive do

604 619
                  @projection["independence"]["private_data"]["issue"]
605 620
                )}
606 621
              </li>
622
              <li :if={@projection["independence"]["deployment"]} id="status-independence-deployment">
623
                deployment: {deployment_text(@projection["independence"]["deployment"])}
624
              </li>
607 625
            </ul>
608 626
609 627
            <ul
test/openagents/forge/independence_disclosure_test.exs modified +157

@@ -17,6 +17,7 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

17 17
  alias OpenAgents.DataRights.ExportInventory
18 18
  alias OpenAgents.Forge.Anchor
19 19
  alias OpenAgents.Forge.Independence
20
  alias OpenAgents.Forge.Repos
20 21
21 22
  # Every string the disclosure may contain. A repository path, an account id,
22 23
  # a node name, or a commit sha reaching this projection fails here, which is

@@ -28,6 +29,7 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

28 29
    tamper_evident
29 30
    tamper_evident_published
30 31
    /.well-known/openagents-forge-anchor.json
32
    refs/heads/main
31 33
    portable partial blocked not_user_data
32 34
  )
33 35

@@ -161,6 +163,99 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

161 163
    end
162 164
  end
163 165
166
  describe "the disclosure publishes its own distance from the proven revision" do
167
    # #187 ran the forge 57 commits behind `main` for long enough for six exit
168
    # surfaces to be proven and absent at the same time, and every tip-shaped
169
    # check stayed green. The distance is the disclosure's own margin of error,
170
    # so it is derived from the repository this forge serves rather than
171
    # restated.
172
    setup :bare_repository
173
174
    test "the distance is the commit count between the running revision and the served head",
175
         %{shas: shas} do
176
      # Three commits on `refs/heads/main`. A node running the first is two
177
      # behind, a node running the head is current, and each answer is a count
178
      # `git` produced, not a number this test also computes by hand.
179
      assert %{"known" => true, "behind" => 2} = Independence.deployment(Enum.at(shas, 0))
180
      assert %{"known" => true, "behind" => 1} = Independence.deployment(Enum.at(shas, 1))
181
      assert %{"known" => true, "behind" => 0} = Independence.deployment(Enum.at(shas, 2))
182
    end
183
184
    test "the ref the distance is measured to is named" do
185
      # A distance with no ref names nothing. `RELEASE-004` binds the proof
186
      # matrix to the candidate sha before `.githooks/pre-push` lets it reach
187
      # this ref, which is what makes its head the proven revision.
188
      assert Independence.projection()["deployment"]["proven_ref"] == "refs/heads/main"
189
    end
190
191
    test "a revision this forge never accepted reports no distance" do
192
      assert %{"known" => false, "behind" => nil} =
193
               Independence.deployment(String.duplicate("a", 40))
194
    end
195
196
    test "a running revision that is not a commit at all reports no distance" do
197
      # The packaged image reports `"image"` until a deployment commits a sha.
198
      # That is a real state, and it must withhold rather than guess.
199
      assert %{"known" => false, "behind" => nil} = Independence.deployment("image")
200
      assert %{"known" => false, "behind" => nil} = Independence.deployment(nil)
201
    end
202
203
    test "a forge that does not serve its own repository reports no distance", %{shas: shas} do
204
      # The bound this section names. A forge withholding its own repository
205
      # reports nothing here, exactly as `EXIT-005` and `EXIT-006` decline to
206
      # detect a forge that withholds a log.
207
      previous = Application.get_env(:openagents, :forge_repos)
208
      Application.put_env(:openagents, :forge_repos, ["served-by-nobody"])
209
      on_exit(fn -> Application.put_env(:openagents, :forge_repos, previous) end)
210
211
      assert %{"known" => false, "behind" => nil} = Independence.deployment(Enum.at(shas, 0))
212
    end
213
214
    test "neither revision the distance lies between reaches the projection", %{shas: shas} do
215
      # `STATUS-001` keeps commit shas off this page, and adding one was among
216
      # the six mutations `EXIT-006`'s proof was confirmed against. A distance
217
      # is a number and fits; the revisions it is a distance between do not.
218
      published = strings(Independence.projection()) ++ strings(Independence.deployment(hd(shas)))
219
220
      for sha <- shas, published_value <- published do
221
        refute String.contains?(published_value, sha)
222
        refute String.contains?(published_value, String.slice(sha, 0, 12))
223
      end
224
    end
225
226
    test "the distance is read from the served repository rather than asserted" do
227
      # The same compiled-import-table read `export_recipient_encryption` uses.
228
      # `OpenAgents.Forge.Repos` is reached from this module for one reason —
229
      # counting the commits between the running revision and the served head —
230
      # so a hardcoded distance loses this call in the same commit.
231
      {:ok, {module, [imports: imports]}} =
232
        :beam_lib.chunks(:code.which(Independence), [:imports])
233
234
      assert module == Independence
235
236
      assert OpenAgents.Forge.Repos in Enum.map(imports, &elem(&1, 0)),
237
             "EXIT-006 derives the deployment distance from a git read of the repository " <>
238
               "this forge serves. A disclosure that stopped reading it must stop " <>
239
               "publishing a distance."
240
    end
241
242
    test "being behind is not an independence shortfall" do
243
      # Deliberate, and recorded in EXIT-006: a node one commit behind is not
244
      # less independent, and folding ordinary deploy lag into `degraded` would
245
      # make the verdict mean nothing on the day it mattered. The distance is
246
      # published beside the verdict, never inside it.
247
      clean_export = %{"gaps" => []}
248
      clean_verification = %{"anchor_published" => true, "anchor_witnessed" => true}
249
250
      clean_private_data = %{
251
        "export_recipient_encryption" => true,
252
        "encrypted_at_rest" => true
253
      }
254
255
      refute Independence.degraded?(clean_export, clean_verification, clean_private_data)
256
    end
257
  end
258
164 259
  describe "the disclosure carries no content" do
165 260
    test "every string in the projection is a family name, a status, or fixed vocabulary" do
166 261
      families = Enum.map(ExportInventory.entries(), &Atom.to_string(&1.family))

@@ -182,6 +277,11 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

182 277
      assert body["independence"]["degraded"]
183 278
      assert body["independence"]["operator"]["model"] == "single_operator"
184 279
      refute body["independence"]["operator"]["mirror_is_authority"]
280
281
      # A check reads the distance here without rehearsing a single exit
282
      # surface, which is what #246 asks of this section.
283
      assert body["independence"]["deployment"]["proven_ref"] == "refs/heads/main"
284
      assert is_boolean(body["independence"]["deployment"]["known"])
185 285
    end
186 286
187 287
    test "the status page names the degraded state and every gap", %{conn: conn} do

@@ -194,6 +294,10 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

194 294
      assert has_element?(view, "#status-independence-private-data")
195 295
      refute has_element?(view, "#status-independence-anchor-witness")
196 296
297
      # A person reads the distance on the page for the same reason: #187 was
298
      # found by hand, one surface at a time, and nothing said a word.
299
      assert view |> element("#status-independence-deployment") |> render() =~ "refs/heads/main"
300
197 301
      rendered = render(view)
198 302
199 303
      for entry <- ExportInventory.with_status(:partial) do

@@ -219,6 +323,59 @@ defmodule OpenAgents.Forge.IndependenceDisclosureTest do

219 323
    end
220 324
  end
221 325
326
  # A bare repository this node serves, with three commits on `refs/heads/main`
327
  # and no working tree — the shape `OpenAgents.Forge.Repos` maintains. The
328
  # commits are built with plumbing so the fixture never depends on a checkout.
329
  defp bare_repository(_context) do
330
    base = Path.join(System.tmp_dir!(), "independence-#{System.unique_integer([:positive])}")
331
    File.mkdir_p!(base)
332
333
    previous_data = Application.get_env(:openagents, :forge_data_dir)
334
    previous_repos = Application.get_env(:openagents, :forge_repos)
335
    Application.put_env(:openagents, :forge_data_dir, base)
336
    Application.put_env(:openagents, :forge_repos, ["disclosure-fixture"])
337
338
    on_exit(fn ->
339
      restore(:forge_data_dir, previous_data)
340
      restore(:forge_repos, previous_repos)
341
      File.rm_rf(base)
342
    end)
343
344
    path = Repos.ensure_repo!("disclosure-fixture")
345
    {tree, 0} = Repos.git(path, ["hash-object", "-w", "-t", "tree", "/dev/null"])
346
    tree = String.trim(tree)
347
348
    shas =
349
      Enum.reduce(1..3, [], fn index, acc ->
350
        parent = if acc == [], do: [], else: ["-p", List.last(acc)]
351
352
        {sha, 0} =
353
          Repos.git(path, ["commit-tree", "-m", "commit #{index}", tree] ++ parent,
354
            env: commit_identity()
355
          )
356
357
        acc ++ [String.trim(sha)]
358
      end)
359
360
    {_output, 0} = Repos.git(path, ["update-ref", "refs/heads/main", List.last(shas)])
361
362
    %{shas: shas}
363
  end
364
365
  defp commit_identity do
366
    [
367
      {"GIT_AUTHOR_NAME", "proof"},
368
      {"GIT_AUTHOR_EMAIL", "proof@example.test"},
369
      {"GIT_AUTHOR_DATE", "2026-01-01T00:00:00Z"},
370
      {"GIT_COMMITTER_NAME", "proof"},
371
      {"GIT_COMMITTER_EMAIL", "proof@example.test"},
372
      {"GIT_COMMITTER_DATE", "2026-01-01T00:00:00Z"}
373
    ]
374
  end
375
376
  defp restore(key, nil), do: Application.delete_env(:openagents, key)
377
  defp restore(key, value), do: Application.put_env(:openagents, key, value)
378
222 379
  defp strings(value) when is_map(value), do: value |> Map.values() |> Enum.flat_map(&strings/1)
223 380
  defp strings(value) when is_list(value), do: Enum.flat_map(value, &strings/1)
224 381
  defp strings(value) when is_binary(value), do: [value]
test/openagents/network_status_test.exs modified +7

@@ -92,6 +92,13 @@ defmodule OpenAgents.NetworkStatusTest do

92 92
    "generated_at",
93 93
    "independence",
94 94
    "independence.degraded",
95
    # The disclosure's distance from the revision its proofs ran against
96
    # (#246). A count and a ref name: the two revisions it lies between are
97
    # commit shas, which this page does not publish.
98
    "independence.deployment",
99
    "independence.deployment.behind",
100
    "independence.deployment.known",
101
    "independence.deployment.proven_ref",
95 102
    "independence.document",
96 103
    "independence.export",
97 104
    "independence.export.blocked",

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