Skip to content

ci(verifiability): verify our own public claims nightly, as an outsider - #9973

Merged
JSONbored merged 5 commits into
mainfrom
ci/nightly-anonymous-verification-9724
Jul 31, 2026
Merged

ci(verifiability): verify our own public claims nightly, as an outsider#9973
JSONbored merged 5 commits into
mainfrom
ci/nightly-anonymous-verification-9724

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

What this is

A scheduled job that is the stranger the docs invite. It runs the verifier published on npm against production, from a runner holding no credentials of any kind, and turns a regression in the public verification path into our problem within a cycle instead of an outside reader's discovery.

Both design choices are load-bearing:

  • The published tool, not this tree. npx -p @loopover/mcp resolves the npm release rather than building from the checkout. What is asserted is "an outsider succeeds today", and an outsider has the published CLI — so a fix that is merged but unpublished must not turn this green. fairness: a corpusChecksum is published for a rule whose corpus is not downloadable #9962 was exactly that: the shipped verifier asked /v1/public/eval-corpus?rule_id= while the route reads ruleId, every corpus fetch 400'd, and the tool reported production as unverifiable while production was fine. Both sides were individually tested and individually correct. Only running the real pair catches it.
  • No token on the verify step. Job permission is contents: read; issues: write and the token exist only on the step that files the tracking issue, after verification finishes. A verifier running with repo credentials is not reproducing an outsider's view and could pass on access a stranger does not have.

Both surfaces are checked (#9940): api.loopover.ai for aggregates and eval-score records, shots.loopover.ai for the anchored ledger. Checking one leaves the other free to regress silently.

Self-verification found a false-green

Per the agreed approach, I pointed the real verifier at a deliberately wrong base URL rather than staging a broken config. It found a bug in my first draft of the decision logic:

$ loopover-verify --base-url https://api.loopover.ai/definitely-not-the-api --json
skip  record-digests      /v1/public/eval-scores unavailable (HTTP 404)
skip  corpus-commitments  /v1/public/eval-scores unavailable (HTTP 404)
skip  anchor-checkpoint   no signed checkpoint published, and the ledger size is unknown
skip  stats-parity        /v1/public/stats unavailable (HTTP 404)
exit=0

Four legitimate skips and a clean exit. Judged claim-by-claim that looks entirely healthy — the monitor would have reported green against a completely dead endpoint, which is the precise failure this job exists to prevent.

The verifier is right to call them skips: from inside a single claim, "this surface is disabled" and "this host is wrong" are indistinguishable. The monitor has the context the claim lacks — production is supposed to be serving these — so a run that positively verified nothing is now red.

The floor is one passing claim, not a proportion, because the two surfaces are legitimately lopsided. Measured against production right now:

Surface pass skip
api.loopover.ai record-digests, corpus-commitments, stats-parity anchor-checkpoint (ledger empty by design, #9940)
shots.loopover.ai anchor-checkpoint the three stats/eval claims (publishes no stats)

"Most claims pass" would fail the Orb every healthy night.

End-to-end result

Input Verdict
Both real production surfaces VERIFY_OK=true4 passed, 0 failed, 4 skipped
Wrong base URL + healthy Orb VERIFY_OK=false — names which surface is dead; the healthy one still passes

Restoring the URL goes green, which is what closes the tracking issue — the recovery half of the acceptance criterion.

Other false-greens rejected

  • a run that produced no readable report (CLI crashed, malformed JSON, npx could not resolve the package): nothing failed because nothing ran;
  • a non-zero exit with no failing claim — two contradictory signals, and the comfortable one is wrong;
  • an unreadable capture file, and a non-numeric exit code, which is treated as failure rather than an optimistic zero.

Skips alone never file an issue — the API's ledger skips nightly by design, and a monitor that files an outage on day one is muted by week one — but they are always listed, since a new skip beside a failure is often its cause.

Tracking issue lifecycle

One issue, found by exact title: filed on failure, commented on while it persists, closed automatically on recovery. Comments rather than body rewrites, because which claim failed on which night is the useful history. The filing step runs before the step that fails the run, so a red job always leaves the issue behind — a monitor that notices a problem and then drops it is worse than none.

Why the logic is a script

scripts/verify-public-claims-report.ts is pure and unit-tested (30 tests), because the judgement of what counts as a regression must not be discovered to be wrong on the night it first fires. It also caught an output-interleaving bug: the workflow wrapping the script's stdout in a heredoc while the script separately appended its scalars buried VERIFY_OK inside the body block — the workflow would have read an empty verdict, which is not "true", and filed an outage on a healthy night. The script now writes all outputs itself, in one ordered write, and refuses to emit a body containing the delimiter.

Docs

A status line and badge on the walkthrough page, which is the surface the issue asks for: green means an outsider succeeded within 24 hours, red means we already know and there is an issue naming the broken claim. The badge links to every run including the failures.

Not included — needs your call

workflow_dispatch takes api-base-url, orb-base-url and dry-run inputs, so the job can be pointed anywhere on demand.

PagerDuty is not wired. The issue asks to notify "via the existing alerting path (PagerDuty)", but that path is Alertmanager on the Orb — there is no PagerDuty secret in GitHub Actions, and no workflow references one. Adding it means introducing a new repository secret, which is an operator decision rather than something to slip into this PR. The tracking issue plus the red run is the notification today. Happy to add the routing step as a follow-up if you want the secret provisioned.

Verification

  • actionlint, lint:composite-actions, typecheck, docs:drift-check, dead-source-files:check, dead-exports:check, import-specifiers:check, coverage-boltons:check, fixture-clock-races:check, checkers-wired:check, validate:no-hand-written-js, ui:typecheck, ui:test — all green
  • npx vitest run test/unit1299 files, 25347 tests passed

Closes #9724

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 supply chain issue(s)

Comment thread .github/workflows/verify-public-claims.yml Outdated
@loopover-orb

loopover-orb Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

Comment thread .github/workflows/verify-public-claims.yml
@JSONbored

Copy link
Copy Markdown
Owner Author

On the red Superagent Supply Chain Scan — investigated rather than waved through.

It reports "1 actionable risk across 2 changed dependencies". This PR changes no dependency files (git diff origin/main...HEAD --name-only over package.json/package-lock.json is empty); the two "dependencies" are the actions a new workflow necessarily references. Both are pinned to the SHAs this repo already standardises on — actions/checkout@3d3c42e (40 other uses) and actions/setup-node@8207627 (31 other uses) — so there is no drift to correct. The same check failed on #9957 and that merged, so it is non-blocking here.

The scan did surface something real, though, and it is fixed in 7970a20b8: the first draft ran the report script via npx --yes tsx, pulling an unpinned TypeScript runner from the registry at job time. That was pure added supply-chain surface for a script that is ours and already checked out. It now runs under node --experimental-strip-types, the same invocation npm run actionlint uses, which removes the fetch entirely.

That leaves exactly one intentional unpinned fetch — npx -p @loopover/mcp loopover-verify — and pinning it would defeat the job: resolving whatever an outsider actually gets from npm today is the property being asserted. A pinned verifier would keep passing after the published one broke, which is the #9962 failure mode this job exists to catch.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 1.55kB (0.02%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.93MB 1.55kB (0.02%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-C4Jd1xF_.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-tUYas_Et.js (New) 955.9kB 955.9kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-Ay3AmhLm.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-BAkUhUm2.js (New) 201.69kB 201.69kB 100.0% 🚀
assets/modal-D9gadMJo.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-C7LphEUm.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-C-uzyZYn.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/verify-this-review-BMsWjZda.js (New) 66.83kB 66.83kB 100.0% 🚀
assets/routes-Bji5jMWD.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/owner-panel-DBaIlY6Z.js (New) 28.26kB 28.26kB 100.0% 🚀
assets/app-GZXCuWmM.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-BUXuSR-K.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-48aamFD3.js (New) 23.13kB 23.13kB 100.0% 🚀
assets/miner-panel-DZtKw2NS.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-BX8A-ddB.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-QHCg7cHC.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-Dl6OrwfY.js (New) 15.68kB 15.68kB 100.0% 🚀
assets/playground-panel-DD8L9eop.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-B2-qUEf9.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-BoNfEvnm.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-CE-goaVD.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-D4h6nSOk.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-BGyj5caT.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-DuAlpnrX.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-DePO97V9.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/docs-nav-DiA8SlQF.js (New) 6.19kB 6.19kB 100.0% 🚀
assets/digest-panel-CuqBCkyo.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BChG3GKF.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-CmTvOruA.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-D3Xr-d8o.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-CZeODW4E.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-HJdIgHwa.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-BvMYM-f_.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-C2Yif8iB.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-Cb4XMuhw.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CtLCJ0Th.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-IJMPb2dk.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BoO5G_tl.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-B0j5uMfR.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-EML2Zm30.js (New) 549 bytes 549 bytes 100.0% 🚀
assets/app.maintainer-Dmt5ct2y.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-BSWmRoO5.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BIfjvkIW.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-CsgiZbnf.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-DctyQiJ0.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-BUIGDC1-.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-B0SLvmDg.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-D831z0nA.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CFiRWuG3.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-1PB99Zti.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-Cjk9mzNY.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-CMxSyoYK.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BCBOXzpC.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-CP7JcVy6.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-CGzgIx0N.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-BN-Zi_Vc.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-CfWt34uv.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-BjI8YaTm.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-DcvxIYlX.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-BA3rS-Mc.js (Deleted) -955.9kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-78Gl3UV8.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-Cp1whQnk.js (Deleted) -201.69kB 0 bytes -100.0% 🗑️
assets/modal-vVY821VG.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-ByY99KGs.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-lXyHVOTY.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/verify-this-review-C8lVDPER.js (Deleted) -65.28kB 0 bytes -100.0% 🗑️
assets/routes-f8O783UM.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-DL0g5So_.js (Deleted) -28.26kB 0 bytes -100.0% 🗑️
assets/app-BhJyXUA8.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-BOtqQLaa.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-DMHUs3nY.js (Deleted) -23.13kB 0 bytes -100.0% 🗑️
assets/miner-panel-D92AUoqu.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-DH7gBuN0.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-CX3WVi-p.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-CDwHm074.js (Deleted) -15.68kB 0 bytes -100.0% 🗑️
assets/playground-panel-Dxhr1BbO.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-DWPK77OC.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-DXlPF16i.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-D1KBd6I0.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BJ_STnlf.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-nuhuuEOk.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-CVumMzxc.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-DgQk1GEa.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/docs-nav-D8dPjj9X.js (Deleted) -6.19kB 0 bytes -100.0% 🗑️
assets/digest-panel-D1YvF5E7.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-DZbubk91.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-DnNV1DX8.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-C2Kt6JpH.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BlJD5nY8.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-BLS0ixvO.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-DSZDbT7n.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-_YNs8cMm.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-DiYSyjx_.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-DMse-DjA.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-DLXf2tTs.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-DSnSqgSy.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-i35fDb3q.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-P9AFQ-46.js (Deleted) -549 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-DjvXUSqz.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-RLRaBzvi.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-C4QH2gJ2.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-CmcW-QwL.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-BgNquQhQ.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CINjQiuc.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-B6TBbmDt.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-CxRy7S9A.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-CxlFHPsW.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-DlqVMHvk.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-DTCQx0ot.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-DQbm1Ph9.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-Cd2RvhJq.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-BVYhiqrl.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BUVJ5MXb.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-mSQ7Oi5x.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-DALSi8i2.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CBmnBpA3.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.67%. Comparing base (910fa59) to head (7c3607b).
⚠️ Report is 7 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9973      +/-   ##
==========================================
+ Coverage   91.88%   95.67%   +3.79%     
==========================================
  Files         928      828     -100     
  Lines      113722    63858   -49864     
  Branches    27436    22257    -5179     
==========================================
- Hits       104489    61095   -43394     
+ Misses       7936     1481    -6455     
+ Partials     1297     1282      -15     
Flag Coverage Δ
backend 95.67% <ø> (ø)
control-plane ?
engine ?
rees ?

Flags with carried forward coverage won't be shown. Click here to find out more.
see 279 files with indirect coverage changes

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 30, 2026
@JSONbored
JSONbored force-pushed the ci/nightly-anonymous-verification-9724 branch from 7970a20 to be2330e Compare July 31, 2026 00:30
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
🔵 In progress
View logs
loopover-ui 85b1d68 Jul 31 2026, 04:46 AM

@JSONbored

Copy link
Copy Markdown
Owner Author

Both Superagent findings triaged.

Security Scan (P2) — real, and fixed in 17e212c3a. The finding was precise: "actions/checkout persists GitHub token to disk, contradicting the 'no credentials on verify step' design." It was right. actions/checkout defaults to persist-credentials: true, which writes an authenticated http.extraheader into .git/config — so withholding the token from the verify step's env: while leaving a usable credential on disk was theatre, and my claim that the step runs with no credentials was false. Now persist-credentials: false. Nothing in the job needs it: the only step that talks to GitHub is the tracking-issue step, which uses gh with an explicit GITHUB_TOKEN in its own environment, and gh reads that variable directly rather than git's credential config.

Supply Chain (HIGH) — not actionable. It flags actions/setup-node@8207627 for threat-filesystem-read, and the cited location is inside the action's own test suite:

setup-node-8207627.../__tests__/authutil.test.ts

That is a test fixture exercising npmrc handling, not behaviour this workflow invokes. The pin is the SHA 31 other workflows in this repo already use, and this PR changes no dependency manifest — the "2 changed dependencies" are simply the two actions any new workflow must reference. There is nothing to remediate short of unpinning from the repo standard, which would be worse.

validate-code will stay red here until #9976 lands — that is the repo-wide collections/* breakage where a node_modules cache hit skips postinstall, unrelated to this PR and currently failing every PR.

LoopOver tells a stranger they can check its fairness claims without asking
permission. Nothing checked that this stayed true, so a regression in the public
verification path was found by outside readers whenever they happened to look.

The job runs the verifier PUBLISHED ON NPM against production, from a runner
holding no credentials. Both parts are load-bearing. Building from the checkout
would assert something weaker -- an outsider has the published CLI, so a fix
merged but unpublished must not turn this green. #9962 is exactly that gap: the
shipped verifier asked for `?rule_id=` while the route reads `ruleId`, every
corpus fetch 400'd, and the tool called production unverifiable while production
was fine. Both sides were individually tested and individually correct; only
running the real pair catches it. The verify step is given no token at all --
`issues: write` lives on the step that files the tracking issue, after
verification is done -- because a verifier running with repo credentials is not
reproducing an outsider's view.

Both surfaces are checked (#9940): `api.loopover.ai` for aggregates and
eval-score records, `shots.loopover.ai` for the anchored ledger.

One tracking issue, found by exact title: filed on failure, commented on while
it persists, closed automatically on recovery. Comments rather than body
rewrites, since which claim failed on which night is the useful history.

Self-verified by pointing the real verifier at a deliberately wrong base URL,
which found a false-green in the first draft of the decision logic. Every claim
came back `skip` ("unavailable (HTTP 404)") and the CLI exited 0 -- judged
claim-by-claim, four legitimate skips and a clean exit, so the monitor would
have reported GREEN against a completely dead endpoint. The verifier is right to
call them skips; from inside one claim, "disabled surface" and "wrong host" are
indistinguishable. The monitor has the context the claim lacks, so a run that
positively verified NOTHING is now red. The floor is one passing claim rather
than a proportion because the two surfaces are legitimately lopsided: measured
against production, the API passes three of four and the Orb passes one of four,
and "most claims pass" would fail the Orb every healthy night.

Three further false-greens are rejected: a run that produced no readable report
(nothing failed because nothing ran), a non-zero exit with no failing claim, and
an unreadable capture file. Skips alone never file an issue -- the API's ledger
is empty by design and skips nightly -- but are always listed, since a new skip
beside a failure is often its cause.

The decision logic is a tested script rather than inline bash. That is also how
an output-interleaving bug was caught: the workflow wrapping the script's stdout
in a heredoc while the script appended its own scalars buried VERIFY_OK inside
the body block, leaving the workflow reading an empty verdict and filing an
outage on a healthy night. The script now writes all outputs itself, in order.

Closes #9724
Drops the second unpinned registry fetch from the nightly job. The script is
ours and already checked out, so pulling an unvetted TypeScript runner from npm
to read it added supply-chain surface for nothing -- flagged by the supply-chain
scan as an actionable risk across the two dependencies the workflow introduced.

The remaining fetch, the published verifier itself, stays unpinned on purpose:
resolving what an outsider actually gets is the property the job asserts.

Same invocation `npm run actionlint` already uses for its own TS script.
Superagent flagged the contradiction, and it was right: actions/checkout
defaults to persist-credentials: true, which writes an authenticated
http.extraheader into .git/config. This job's entire claim is that it reproduces
what an anonymous stranger sees, so withholding the token from the verify step's
env while leaving a usable credential in .git/config is theatre -- anything the
step runs could still pick it up.

Nothing in the job needs it. The only step that talks to GitHub is the
tracking-issue step, which uses gh with an explicit GITHUB_TOKEN in its own
environment; gh reads that variable directly and never consults git's
credential config.
@JSONbored

Copy link
Copy Markdown
Owner Author

Status: everything passes except Superagent Supply Chain Scan. validate-code, validate, codecov/patch, codecov/project, Workers Builds and Superagent Security Scan are all green.

The Security finding is fixed — it was real, and it was mine: actions/checkout defaults to persist-credentials: true, writing an authenticated header into .git/config, which made this job's "no credentials on the verify step" claim false. Now persist-credentials: false.

The remaining Supply Chain failure is a false positive I do not think should be fixed in code:

HIGH: actions/setup-node@8207627 — Detects access to sensitive files (credentials, configs, keys)
Location: setup-node-8207627.../__tests__/authutil.test.ts

The cited path is a test fixture inside the action's own repository, exercising npmrc handling — not behaviour this workflow invokes. The pin is the SHA 31 other workflows here already use, and this PR changes no dependency manifest: the "2 changed dependencies" are simply actions/checkout and actions/setup-node, which any new workflow must reference. So this fires for every new workflow added to the repo, regardless of content.

I looked at removing the flagged dependency, and decided against it

Dropping actions/setup-node would silence the scan. The job only needs Node to run npx -p @loopover/mcp and one node --experimental-strip-types script, and three workflows here (ci.yml, mcp-release-watch.yml, package-release-watch.yml) already run npx on the runner's ambient Node.

But --experimental-strip-types needs Node ≥ 22.6, and every workflow here that uses it pairs it with setup-node — there is no precedent for running it on the ambient runner Node. Removing the pin would make this nightly the first to depend on the runner image's Node version, untested, for a job that runs unattended at 04:12 UTC. Trading a scanner false positive for a nightly that might silently stop running is a bad trade, and dodging a scanner is not a good enough reason to make the workflow less deterministic.

Options

  • A (recommended): accept/suppress this finding in Superagent — it is a third-party test fixture at the repo-standard pin, and it will recur on every new workflow.
  • B: I drop setup-node and add an explicit Node-version precondition that fails loudly if the runner is too old. Removes the finding; accepts a first-of-its-kind dependency on the runner image.
  • C: fold the nightly into an existing workflow that already references these actions. Silences the scan, but couples an independent schedule and permission set to an unrelated workflow — I would not recommend it.

Happy to do B if you would rather the board be clean; otherwise this is ready to merge as-is.

…a comment

Adjudicating the supply-chain finding on actions/setup-node rather than waving
it through. Checked at the pinned SHA (v7.0.0):

  • the action's entrypoint is `dist/setup/index.js`; the cited
    `__tests__/authutil.test.ts` is a test fixture and is never executed;
  • the credential writer is `auth.configAuthentication(registryUrl)`, and
    `src/main.ts` calls it ONLY inside `if (registryUrl)`.

So the capability is real and unreachable here, because this workflow does not
set `registry-url`. That makes the safety a property of THIS FILE rather than of
the action -- which is the part worth acting on. Adding one innocuous-looking
line would make a job that claims to hold no credentials write an authenticated
.npmrc, and nothing would have noticed.

That is not hypothetical: the same class already shipped in this file and was
caught in review, when actions/checkout's `persist-credentials` default left a
usable token in .git/config while the job asserted it had none.

So the four credential paths are now asserted against the shipped workflow
instead of described in a comment -- the codebase's own standard for a snapshot
presented as a guarantee:

  • checkout must set `persist-credentials: false` (asserted explicitly, not
    absent-or-false, which would pass on the broken default);
  • setup-node must not be given a `registry-url`;
  • the verify step must have NO env at all -- not "no token", since an
    allowlist of forbidden names misses the next secret someone adds;
  • exactly one step may hold a token, and it must be the tracking-issue step;
  • no step may run `npm ci`, which would verify against this checkout instead
    of the published package.

Mutation-tested: restoring persist-credentials, adding registry-url, putting a
token on the verify step, and adding npm ci each fail. The suite also asserts it
found a verify job with steps, so it cannot pass vacuously against an empty
array if the file is renamed.
@JSONbored

Copy link
Copy Markdown
Owner Author

Adjudicated at the pinned SHA, and hardened where it actually mattered

The finding names a real capability that is unreachable in this workflow. Verified against actions/setup-node at 820762786026740c76f36085b0efc47a31fe5020 rather than assumed:

  • The SHA is the official v7.0.0 tag.
  • action.yml entrypoint is dist/setup/index.js (+ dist/cache-save/index.js). The cited __tests__/authutil.test.ts is a test fixture and is never executed by the action.
  • The credential writer is auth.configAuthentication(registryUrl), and src/main.ts calls it only inside a guard:
    const registryUrl: string = core.getInput('registry-url');
    if (registryUrl) {
      auth.configAuthentication(registryUrl);
    }
    This workflow does not set registry-url, so the .npmrc-writing path is not reached.

So there is no vulnerability to fix in the action or the pin. But the audit surfaced something that is worth fixing: the safety is a property of this file, not of the action. One innocuous line — registry-url: — would make a job that claims to hold no credentials write an authenticated .npmrc, and nothing would have noticed.

That is not hypothetical. The same class already shipped in this very file and was caught in review: actions/checkout's persist-credentials default left a usable token in .git/config while the job asserted it had none.

What changed (a69c16c5f)

The four credential paths are now asserted against the shipped workflow instead of described in a comment:

Guarantee Mutation Result
checkout sets persist-credentials: false set it back to true 1 failed
setup-node has no registry-url add one 1 failed
the verify step has no env at all add a token 2 failed
no step runs npm ci add one 1 failed

Asserted explicitly rather than absent-or-false, because persist-credentials defaults to true and an absent-or-false check would pass on the broken default. And "no env at all" rather than "no token", because an allowlist of forbidden names misses the next secret someone adds. The suite also asserts it found a verify job with steps, so it cannot pass vacuously if the file is renamed.

The audit is recorded on the step itself, pointing at the test rather than asking the next reader to trust the comment.

The scan will still show red

Nothing here changes what it reports: it flags the two actions any new workflow must reference, and the HIGH is a test fixture in a third-party repo at the SHA 31 other workflows already pin. I stand by not removing setup-node--experimental-strip-types needs Node ≥ 22.6, every workflow here that uses it pairs with setup-node, and making this nightly the first to depend on the runner image's Node version would trade a scanner false positive for a job that might silently stop running. Options A/B/C from my previous comment still stand; A remains my recommendation.

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

{
echo "### Anonymous verification — ${{ steps.verify.outputs.VERIFY_SUMMARY }}"
echo ""
echo "- \`${{ env.API_BASE_URL }}\`"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Template injection in Report verdict step via workflow_dispatch inputs

workflow_dispatch inputs are interpolated into a shell run block with backtick command substitution.

Use shell variables ($API_BASE_URL, $ORB_BASE_URL) instead of template substitution.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name=".github/workflows/verify-public-claims.yml">
<violation number="1" location=".github/workflows/verify-public-claims.yml:136">
<priority>P1</priority>
<title>Template injection in Report verdict step via workflow_dispatch inputs</title>
<evidence>The Report verdict step interpolates ${{ env.API_BASE_URL }} and ${{ env.ORB_BASE_URL }} directly into a shell script. These values derive from ${{ inputs.api-base-url }} and ${{ inputs.orb-base-url }}, which are workflow_dispatch inputs. The surrounding backticks create a bash command-substitution context, allowing arbitrary command execution when a malicious input is provided.</evidence>
<recommendation>Replace the template substitutions with shell variable references ($API_BASE_URL, $ORB_BASE_URL). These environment variables are already set at the job level, so this simply removes the template injection vector.</recommendation>
</violation>
</file>

…write credentials

Resolves the P1 supply-chain finding by removing the dependency rather than
arguing with it.

The finding was accurate about the capability and, as far as this workflow goes,
unreachable: `actions/setup-node` writes an authenticated .npmrc through
`auth.configAuthentication(registryUrl)`, and at the pinned SHA (v7.0.0)
`src/main.ts` calls it only inside `if (registryUrl)` -- an input this job never
set. The cited `__tests__/authutil.test.ts` is a test fixture; the action's
entrypoint is `dist/setup/index.js`.

But "unreachable because nobody has added one line yet" is a weaker property
than "not present", and for a job whose entire claim is that it holds no
credentials, not present is the one worth having. This job needs a Node RUNTIME,
not the repository's toolchain: it runs a published CLI through npx and one
checked-in script, and builds nothing. The runner image already provides node
and npm, and ci.yml, mcp-release-watch.yml and package-release-watch.yml already
rely on that.

The tradeoff is that the Node version is the runner's rather than .nvmrc's, so
it is ASSERTED instead of assumed. `--experimental-strip-types` needs 22.6+, and
a precondition step fails immediately with the remedy in the message -- re-add
actions/setup-node with node-version-file -- if an image ever regresses below
that. A nightly that breaks quietly is the failure mode worth spending a step on.

checkout is now the only third-party action in the job, and the tests assert
that as an ALLOWLIST rather than a blocklist of known-risky ones: a new action
arriving here should be a deliberate decision, not something a scanner notices
later.

Also pins the dry-run gate raised alongside the persist-credentials finding. The
disk half is closed by `persist-credentials: false`; this asserts the other half,
so the gate cannot be dropped while the token-bearing step remains.

Mutation-tested: re-adding setup-node fails 3 assertions, and removing the
dry-run gate fails its own.
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored
JSONbored merged commit 5ee37c4 into main Jul 31, 2026
8 checks passed
@JSONbored
JSONbored deleted the ci/nightly-anonymous-verification-9724 branch July 31, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(verifiability): nightly anonymous walkthrough run — fail loudly on any public-path regression

1 participant