Perform the five exit rehearsals that have never been run #180
Evidence
Shipped in the release at 0bf2706, promoted 2026-08-27.
1 pushes receipt
- AtlantisPleb opened this issue 6d ago
-
AtlantisPleb
closed this as completed in
619260d5d ago -
A Author 5d ago Rehearsals 2 through 6 were performed on 2026-08-25 against the live forge on revision
46cf8a5aea3791936c22e82c145a9a8dd734374d, deployed on all three fleet nodes.docs/2026-08-25-forge-exit-rehearsals-2-to-6.mdrecords every step, its output, and its verdict, and the status table indocs/forge-exit-rehearsals.mdnow names a date and a result on every row.Rehearsal Verdict Filed 2. Receipt verification Steps 1 and 3 pass live; step 2 local only #251; #190 still reproduces 3. Mirror divergence Steps 1 and 2 pass live and confirm #188; step 3 local only none new 4. Key rotation Passes in the test process; live half is a read-only inventory #253 5. Operator loss Recovery passes in the test process; the limit is now measured none new 6. Partial export Passes live; was red on main#252 What each one produced
Rehearsal 2.
verify/2on a fleet node reportsfindings: []over 382 entries withchained_from: 279and a head link at seq 381. The anchor published at/.well-known/openagents-forge-anchor.jsonwas fetched over HTTPS first and then used as the anchor: the published%{seq: 375, link: "19c7a2c5…"}verifies clean, one changed hex digit reportsanchor_mismatch, a sequence belowchained_fromreportsanchor_unreachablewith"entry carries no link", and a sequence past the end reports it with"no entry at this sequence". Step 2 needs tampering, so it ran against a forge the test process builds — 37 tests, all green, naming each finding code. No tamper was performed against production.Rehearsal 3. Both remotes advertise the same 25 refs byte for byte,
refs/heads/mainidentical. Full clones of each: the forge serves 423 commits onmain,git fsckclean, grafted at fiveshallowboundaries witheda094c6as its root; GitHub serves 730, clean, rooted ata352f78e. The 307-commit gap is exactly #188 at today's numbers, and the node confirmsopenagents.comas the one configured mirror. Step 3 callsrebuild/1, which discards a projection, so it was not run against production.Rehearsal 4. This rehearsal already has an executable proof, which the runbook recorded as "None":
test/openagents/forge/key_rotation_test.exs, 12 tests, all green. It covers both halves of this issue's contract — the WAL chain link is unkeyed so no rotation invalidates a receipt, and a backward retirement is refused naming the attestation it would have unverified, as are one before a silent key's activation and one beyond the clock-skew allowance. That closes the third acceptance criterion. No key was rotated against production; the live half is an inventory read without printing key material, and it found thatMACHINE_TOKEN_ENCRYPTION_KEYis unset (#253), the GitHub retired keyring is empty, and no reputation issuer key is admitted — so neither rotation has ever run here.Rehearsal 5. Recovery from the WAL passes in the test process. The limit this issue asks to be stated plainly is now measured rather than asserted, and it is stronger than the runbook reads: the WAL adapter is
OpenAgents.Forge.WAL.Gcsagainst one bucket in the operator's own cloud project with uniform bucket-level access, and that bucket has no object versioning — soft delete retains a deleted object for seven days and nothing longer. The storage keeps no history of itself, so an operator who rewrote an entry a week ago leaves no earlier copy in it. The only commitment outside that bucket is the anchor, served by the same operator and witnessed by nobody (#151).Rehearsal 6.
GET /api/statuspublishes 35 families — 24 portable, 0 partial, 1 blocked, 10 not user data — and the deployed ledger reports the same split with no drift. Steps 1 and 2 were exercised through the route against a local forge: 14 caps inbounds, 10*_truncatedflags, 4 families innot_includedeach with a mechanism and a reason, and a sealed?recipient=variant that decrypts to the same bounds. One flag was observed positive — a 70,000-byte Box run exports 65,536 withoutput_truncated: trueandoutput_byte_size: 70000— which no committed test does, since every*_truncatedassertion in the suite is arefute. That proof gap is recorded rather than closed.Defects filed
- #251 — the three nodes disagree about the same repository at the same instant. The WAL is shared and the projection is node-local, so a node that has not replayed an entry yet reports
served_refs_divergedandobject_missing, which are the findings that mean tampering. Reproduced twice, minutes apart, in both directions. It converges on its own within minutes. It blocks #179's third acceptance criterion, because a scheduled pass built today would publish tampering findings for a healthy forge. - #253 — production never provisioned
MACHINE_TOKEN_ENCRYPTION_KEY, soconfig/runtime.exsstill falls through to the boot bridge and the pairing vault runs on the GitHub vault's key. #192's fix is deployed and correct; the secret it needs was not set, soVAULT-001's separation is not deployed and this runbook's "rotating the GitHub key no longer touches this vault" was false against production. An owner action rather than a code change. - #252 —
POST /api/v1/responsesreachedOpenAgentsWeb.ApiRouteAuthoritywith the family:responseand no ledger entry, soEXIT-001's derived coverage was red onmain. Answered rather than carried, because a red onmainis not a finding to file and leave: the stub reaches no repo and no schema, so the family isnot_user_datawith a note saying the entry is revisited the day a provider stands behind the route.
#190 still reproduces:
verify/2now accepts anowner/namepath, which is its first acceptance criterion, butOpenAgents.Forge.Repos.allowed_repos/0still returns a name no verifier can use and the staleopenagents.com.gitbare repository still sits beside the real ones.What was deliberately not done
Every rehearsal was read-only against production. Nothing deleted a repository, force-pushed, mirrored, rotated a credential, or wrote to the WAL. Three steps mutate state — rehearsal 2's tampering, rehearsal 3's rebuild, and any rotation in rehearsal 4 — and each ran against a forge the test process builds, labeled local in the record. A rehearsal that cannot safely run its own destructive step against production has a permanent gap, and the record says so rather than implying the step was taken.
Commit
619260d. - #251 — the three nodes disagree about the same repository at the same instant. The WAL is shared and the projection is node-local, so a node that has not replayed an entry yet reports
Problem
docs/forge-exit-rehearsals.mddefines six rehearsals against the forge's exit properties. One has been performed. Five have not, and the status table in that document says so rather than implying otherwise.The one that was performed found a real defect on the first step (#179), which is the argument for performing the rest:
EXIT-001throughEXIT-006were all green while the live forge could not serve a full clone of its own repository, because every proof runs against a forge the test process builds.EXIT-004,EXIT-001EXIT-002,EXIT-005EXIT-003EXIT-003EXIT-001Rehearsal 4 has no executable proof at all. Rotating the forge operator token, an account's
oa_pat_tokens, the reputation issuer key, and the three hand-rolled vaults are four different procedures with four different failure modes, and none of them has been exercised.Contract
docs/forge-exit-rehearsals.md— including the ones that fail. A rehearsal that finds nothing is a result; a rehearsal that is skipped is not.Acceptance criteria
Parent: #94.