fix(ledger): anchoring is a self-host capability — correct the docs and ship the missing keypair runbook (#9719) - #9761
Conversation
…nd ship the missing keypair runbook (#9719) #9719 diagnosed the topology precisely and it holds: hosted review execution is retired (src/index.ts:61-75 acks and drops review jobs), so api.loopover.ai never decides anything, its ledger is permanently empty, and `runScheduledLedgerAnchor` early-returns `empty_ledger` on every tick forever. Verified against the code rather than taken on faith. DECISION on the issue's item 1 -- anchoring lives on self-host, and the docs say so. The alternative (ingest decision records centrally so the hosted Worker has something to anchor) was rejected on the merits: the central chain would be a re-chained AGGREGATE of many instances' records, so anchoring it proves nothing about the per-instance chain a verifier actually cares about, and every real chain would still be unanchored. It would produce a MORE authoritative-looking artifact carrying a WEAKER guarantee -- the exact "looks verified, isn't" failure this epic exists to prevent. The user-facing half of that was a genuine false claim: what-you-can-verify told verifiers to `curl https://api.loopover.ai/v1/public/decision-ledger/ anchors`, an endpoint that returns `[]` forever, as THE anchor walkthrough. Every command now runs against `$ORB` -- the instance that reviewed the PR -- with the boundary and the reasoning stated where the reader hits it. Item 2 was simply missing: no file outside src/ and test/ mentioned LEDGER_ANCHOR at all, so both halves had to be hand-produced in exact encodings, with a keyId that is computeAnchorKeyId of the public half. `npm run ledger:anchor-keygen` now emits both, deriving the id with the same function the runtime uses. The derivation is exported and returns values rather than only printing, because a generator that emits a key the runtime REFUSES would reproduce the original failure while looking like a fix -- six tests run the generated values through the real parse/select/sign/verify path, including that a signature does not verify against a different run's key. Plus the operator runbook that did not exist: what to set, which half is secret, how to read the four `status` values, and the rotation rule that a retired key is closed with notAfter and never removed. Item 3 (make the silence visible) was already closed by #9755's publicAnchorStatus -- checked before duplicating it.
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 07:30:11 UTC
Review summary Nits — 6 non-blocking
Concerns raised — review before merging
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 5bea2bc | Commit Preview URL Branch Preview URL |
Jul 29 2026, 07:07 AM |
Bundle ReportChanges will increase total bundle size by 18.69kB (0.24%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9761 +/- ##
=======================================
Coverage 90.28% 90.28%
=======================================
Files 906 906
Lines 113349 113349
Branches 26890 26890
=======================================
Hits 102335 102335
Misses 9683 9683
Partials 1331 1331
Flags with carried forward coverage won't be shown. Click here to find out more. |


The diagnosis holds
#9719's topology analysis is correct, and I verified it against the code rather than taking it on faith:
src/index.ts:61-75acks and drops review-execution jobs on the hosted Worker, soapi.loopover.ainever decides anything, its ledger is permanently empty, andrunScheduledLedgerAnchorearly-returnsempty_ledgeron every 2-minute tick — forever.Decision on item 1: anchoring lives on self-host, and the docs now say so
The alternative — ingest decision records centrally so the hosted Worker has something to anchor — I rejected on the merits, not on effort. The central chain would be a re-chained aggregate of many instances' records. Anchoring it proves nothing about the per-instance chain a verifier actually cares about, and every real chain would still be unanchored. It would produce a more authoritative-looking artifact carrying a weaker guarantee: exactly the "looks verified, isn't" failure this whole epic exists to prevent.
The user-facing half of this was a genuine false claim.
what-you-can-verify.mdxtold verifiers to run:as the anchor walkthrough — an endpoint that returns
[]forever. In trust documentation that's the highest-severity kind of error. Every command now runs against$ORB, the instance that reviewed the PR, with the boundary and the reasoning stated inline where a reader hits it, plus whatstatustells them before they read the list.Item 2 was simply missing
No file outside
src/andtest/mentionedLEDGER_ANCHORat all. Both halves had to be hand-produced in exact encodings, with akeyIdthat iscomputeAnchorKeyIdof the public half — guess any of it wrong and you gotledger_anchor_skipped_unconfiguredand an empty list.npm run ledger:anchor-keygennow emits both, deriving the id with the same function the runtime uses so the published key and the anchors referencing it cannot disagree.The derivation is exported and returns values rather than only printing them, deliberately: a generator that emits a key the runtime refuses would reproduce the original failure while looking like a fix. Six tests run the generated values through the real
parseAnchorPublicKeys→currentAnchorKey→signLedgerAnchorPayload→verifyLedgerAnchorSignaturepath, including that the key is selectable as current (the specific early return that left anchors empty) and that a signature does not verify against a different run's key.Plus the operator runbook that didn't exist: what to set, which half is secret, how to read the four
statusvalues, and the rotation rule — a retired key is closed withnotAfter, never removed, because a verifier checking a two-year-old checkpoint needs the key it was signed with.Item 3 was already done
publicAnchorStatus(#9755) already distinguishesanchored/pending/empty_ledger/unconfigured. I checked before duplicating it.Verification
6 new tests; 49 passing across the anchor suites.
tsc, docs-drift, import-specifiers, dead-source-files and command-reference all clean. The generator was run for real — its output is a valid P-256 keypair in the documented encodings.Closes #9719