Skip to content

fix(selfhost): use own-property check for retired config-lint fields - #3379

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
galuis116:fix/config-lint-retired-field-own-property-v2
Jul 5, 2026
Merged

fix(selfhost): use own-property check for retired config-lint fields#3379
JSONbored merged 1 commit into
JSONbored:mainfrom
galuis116:fix/config-lint-retired-field-own-property-v2

Conversation

@galuis116

Copy link
Copy Markdown
Contributor

Summary

unknownTopLevelWarnings() in src/selfhost/config-lint.ts splits unknown top-level manifest fields into "retired" (with a migration warning) vs genuinely "unknown", using the JavaScript in operator:

const retiredWarnings = keys.filter((key) => key in RETIRED_FIELD_MIGRATION_WARNINGS).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!);
const unknown = keys.filter((key) => !(key in RETIRED_FIELD_MIGRATION_WARNINGS)).map(formatFieldName);

RETIRED_FIELD_MIGRATION_WARNINGS is a plain object literal, so in walks the prototype chain: "constructor" in obj, "toString" in obj, "hasOwnProperty" in obj, "valueOf" in obj, etc. are all true. A manifest with a field named like an Object.prototype member is therefore misclassified as retired, and RETIRED_FIELD_MIGRATION_WARNINGS["constructor"] resolves to the inherited Object function, which is pushed into a declared string[]. Because JSON.stringify drops function values, that warning serializes to null over any API — and the genuine "Manifest contains unknown top-level field: constructor." warning is silently suppressed, so the suspicious unknown field disappears from operator-facing output.

The sibling recognizedFieldsFor in the same file already does this correctly with Object.prototype.hasOwnProperty.call(...); this call site was the inconsistent one. The fix uses an own-property check. Adds a regression test that lints a constructor: field and asserts the correct unknown-field warning plus that every warning is a string.

No linked issue: issue creation on this repo is restricted to collaborators, and this is a small, self-contained correctness fix (own-property check, matching the sibling in the same file) whose rationale is self-evident from the diff.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked an issue, or this is small enough that the summary explains why an issue is not needed.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

If any required check was skipped, explain why:

  • The full npm run test:ci aggregate ran green plus npm audit --audit-level=moderate (0 vulnerabilities). The changed lines are exercised by the existing retired/unknown-field tests plus the new constructor-named-field regression test (which fails on the old in operator and passes with hasOwnProperty), so codecov/patch is 100% for the diff.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

Pure config-lint logic; it makes operator-facing manifest linting correct for prototype-named fields and exposes no secrets. No auth/CORS/session surface, no API/OpenAPI shape change, no UI.

Notes

  • Three-line change (own-property predicate) plus a regression test. No schema, migration, OpenAPI, wrangler, or generated-artifact impact.

unknownTopLevelWarnings classified retired vs unknown top-level manifest
fields with `key in RETIRED_FIELD_MIGRATION_WARNINGS`, which walks the
prototype chain. A manifest field named like an Object.prototype member
(constructor, toString, hasOwnProperty, valueOf, ...) therefore tested true
for the inherited property and resolved to the prototype's function instead
of a real warning string — corrupting the string[] result (the function
serializes to null over JSON) and suppressing the genuine unknown-field
warning for that suspicious key.

Use Object.prototype.hasOwnProperty.call, matching the sibling
recognizedFieldsFor in the same file. Adds a regression test for a
constructor-named field.
@galuis116
galuis116 requested a review from JSONbored as a code owner July 5, 2026 05:40
@superagent-security

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.89%. Comparing base (113da08) to head (0393ee6).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3379   +/-   ##
=======================================
  Coverage   93.89%   93.89%           
=======================================
  Files         283      283           
  Lines       30573    30574    +1     
  Branches    11138    11138           
=======================================
+ Hits        28705    28706    +1     
  Misses       1211     1211           
  Partials      657      657           
Files with missing lines Coverage Δ
src/selfhost/config-lint.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 5, 2026
@loopover-orb

loopover-orb Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Warning

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

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-05 06:27:04 UTC

2 files · 1 AI reviewer · no blockers · readiness 80/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): src/selfhost/config-lint.ts (matched src/selfhost/**).

Review summary
This change fixes the real source of the retired-field misclassification by replacing prototype-chain membership with an own-property check in `unknownTopLevelWarnings`, matching the existing pattern in `recognizedFieldsFor`. The regression test exercises the production `lintManifestText` path with `constructor`, verifies the unknown-field warning, and guards the string contract for warnings. I do not see a reachable correctness issue in the diff.

Nits — 3 non-blocking
  • src/selfhost/config-lint.ts:79: nit: the multi-line comment is accurate but longer than the local code needs now that the regression test documents the failure mode; consider trimming it to the `key in` vs own-property rationale.
  • src/selfhost/config-lint.ts:82: consider hoisting the own-property helper or using a shared local helper if more retired-field maps are added, so this convention stays consistent with `recognizedFieldsFor`.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ No-issue rationale PR body explains why no issue is linked.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (no linked issue context).
Validation posture ❌ 5/25 Preflight is holding this PR: the review lane is unavailable, so it is not ready for automated review.
Contributor workload ✅ 10/10 Author activity: 1888 registered-repo PR(s), 1249 merged, 59 issue(s).
Contributor context ✅ Confirmed Gittensor contributor galuis116; Gittensor profile; 1888 PR(s), 59 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: galuis116
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 1888 PR(s), 59 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Await review-lane availability.
  • Refresh registry data or choose a registered active repo.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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 Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@JSONbored
JSONbored merged commit 1473f7b into JSONbored:main Jul 5, 2026
9 checks passed
JSONbored added a commit that referenced this pull request Jul 29, 2026
…ync the example template

Two failures from the #9569 manifest block, both mine.

1. The unknown-top-level-field validator never learned about `publicProof`,
   so every manifest carrying it warned "Manifest contains unknown
   top-level field: publicProof." That was invisible on the first pass and
   appeared on every LATER one, because the first pass parses a manifest
   with no such key while later passes reload the persisted snapshot --
   which my loader change now serializes the field into. The warning lands
   in the published review comment, so an unchanged PR got a fresh comment
   PATCH on every regate sweep: exactly the #3379 churn that test exists to
   prevent, reintroduced by a field the writer knew about and the reader
   did not.

   Found by instrumenting the test's PATCH interception to diff the two
   comment bodies rather than guessing at the cause; the added line named
   itself.

2. config/examples/loopover.full.yml must mirror .loopover.yml.example
   from "WHERE IT LIVES" onward, and I documented the block in only one of
   the two.

Verified against origin/main first to confirm both were regressions from
this branch rather than pre-existing.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…ow the gap exposed

Codecov flagged 8 uncovered changed lines across focus-manifest.ts and
routes.ts. I had measured coverage on proof-summary.ts and proof-badge.ts
only, and never on the two files the manifest block and the routes
actually touched -- so the gap was in my own verification, not just the
tests.

Closing it turned up a real defect rather than only missing assertions:
loadProofPageRepoOverride used `.catch()` on the injected manifest loader,
so a loader throwing SYNCHRONOUSLY (a driver-level failure before it ever
returns a promise) skipped the handler entirely and would have escaped to
the route -- 503ing a public page over a manifest read that is supposed to
be optional. That is the same defect this file already had in
loadProofSummary's section reads, which I fixed there and then
reintroduced here. Now a real try/catch, with a regression test using a
synchronously-throwing loader.

Coverage:
- parsePublicProofConfig / publicProofConfigToJson: explicit on/off, a
  present-but-empty block (present-but-false, which the resolver keys on),
  absence, three non-mapping shapes warning rather than throwing, and a
  snapshot round-trip.
- A regression test asserting publicProof is a KNOWN top-level field, so
  the writer/reader split behind the #3379 regate churn cannot return.
- The two route 503 arms are unreachable today (every inner read is
  individually fail-safe), so they are excluded with the house v8 pragma
  and a note on why they are kept: a future unguarded read should degrade
  to 503 rather than 500 on an unauthenticated public route. The badge arm
  uses ignore start/stop -- `next 2` miscounts across a multi-line comment
  and left the return uncovered.

All three changed files now report zero uncovered changed lines.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…#9569) (#9608)

* feat(proof): public per-repo proof summary, endpoint and README badge (#9569)

The shareable, unauthenticated twin of the in-app trust panel. One
composition serves both, so the public page and #9193's panel cannot
disagree about a figure -- which is the property the page exists to
demonstrate.

THE PRIVACY BOUNDARY IS STRUCTURAL. Every field is built by NAMING it,
never by filtering a wider object. A blocklist has to anticipate every
field a future upstream type might grow and silently leaks the one it did
not; an allowlisted shape cannot leak a field nobody wrote down. Tested by
feeding hostile records carrying hotkey/wallet/reward/trust-score/private-
rank and asserting none of it reaches the serialized page -- while the
named fields do, so the test proves allowlisting rather than an empty
object.

NEVER A BARE SCALAR. Any accuracy figure carries its coverage and a Wilson
interval; below a 20-decision floor there is no rate at all, only an
explicit insufficient_data state that still publishes the count. A perfect
record over 19 decisions must not render as 100%. Wilson rather than Wald
because a gate metric lives near p->1, exactly where Wald claims
impossible certainty.

HONEST BOUNDARY STATES. An empty ledger is `empty`, not `verified` --
different claims. A failed read is `unavailable`, not `broken`, which
would accuse the operator of tampering. A FAILED anchor attempt is not an
anchor: the public attempt log is where failures are legible, and
presenting one here would claim corroboration that does not exist. The
verification-contract boundary statement travels IN the payload, so a
screenshot or embed cannot shed it the way a footer caption can.

The badge reports the LEDGER's state rather than an accuracy percentage: a
badge is a one-glance claim, and an accuracy number without the interval
that makes it honest does not fit in one. Disabled and errored both render
a neutral SVG -- a broken image in a README is worse than an honest
"unavailable".

DECISION (requirement 6), recorded beside the code that implements it: the
page is opt-OUT per repo, default ON once the operator's fleet-wide flag
(default OFF) is on. Every figure is already publicly fetchable through
the ledger-verify / anchors / decision-record endpoints, so gating a page
over it would add friction without privacy. The per-repo switch still
exists because a page is a different artifact from an API -- discoverable,
linkable, and it markets a repo's numbers whether or not the maintainer
wants that. A repo can opt out but cannot opt IN when the operator has
not, which keeps the fleet switch a real switch.

Found and fixed while testing: `DB.prepare()` throws SYNCHRONOUSLY on a
driver-level failure, so the `.catch()` chain never ran and a D1 outage
would have 503'd the whole public page instead of degrading. Each section
is now a real try/catch, which is the difference between the
fail-safe-per-section contract being documented and being true.

Backend half of #9569; the /proof/:owner/:repo UI route renders this
payload and lands separately.

* fix(proof): actually wire the per-repo opt-out the routes only claimed to honor (#9569)

Review caught the real defect: both handlers called
isProofPageEnabledForRepo(c.env) with no second argument, so the
ProofPageRepoOverride documented at length in proof-summary.ts and in the
PR body was never loaded or passed. Every repo was effectively
opt-out-less once the fleet flag was on -- a gate that is described,
typed, and unit-tested as a pure function, but never reachable from the
surface it governs. That is the registered-but-unreachable class, and the
long comment made it worse rather than better by making it look done.

- Adds a real `publicProof:` focus-manifest block (engine parser + toJson
  + loader snapshot), mirroring `publicStats:`/`ops:`. Precedence is
  deliberately the opposite of those two: read from the TARGET repo's
  manifest rather than the operator's self-repo, because the thing being
  opted out of is that repo's own page.
- loadProofPageRepoOverride resolves it, degrading a failed manifest load
  to "no override" -- a broken manifest never takes a page DOWN, which is
  the failure direction worth accepting here and is now stated in the
  doc comment rather than left implicit.
- Both routes load the override BEFORE anything else, so a repo that
  turned its page off does not have its decision records queried to build
  a summary that will be discarded.
- Documents the block in .loopover.yml.example, including the precedence
  and the opt-out default.

Tests that would have caught it: a repo opting out in its manifest now
gets 404 from BOTH routes with the fleet flag on, while a different repo
in the same fleet still serves 200 (the opt-out is per repo, not a kill
switch); explicit opt-in and no-block-at-all both serve; and the resolver
is covered across absent/explicit/failing loads.

* fix(build): build @loopover/contract in ui:build, unbreaking the Cloudflare Workers build

The Workers build for loopover-ui has been failing on every PR since #9521
(merged as #9590) made src/openapi/schemas.ts import
@loopover/contract/public-api:

  Cannot find module '.../node_modules/@loopover/contract/dist/public-api.js'
    imported from /opt/buildhome/repo/src/openapi/schemas.ts

ui:build builds ui-kit and engine, then runs ui:openapi -- but never builds
the contract package, so the import resolves to a dist/ that does not
exist. CI did not catch it because the GitHub workflow has its own
separate "Build contract package" step (ci.yml:361) before the drift
checks; the Cloudflare build runs npm run build:cloudflare -> ui:build
directly and gets no such step. The two paths had silently diverged.

Add @loopover/contract to the same turbo invocation that already builds
the engine, so the one script both paths share produces everything
ui:openapi imports.

Reproduced locally by deleting packages/loopover-contract/dist and running
ui:openapi (identical ERR_MODULE_NOT_FOUND), then confirmed the fixed
chain builds the package and writes the spec with no drift.

* fix(manifest): register publicProof as a known top-level field, and sync the example template

Two failures from the #9569 manifest block, both mine.

1. The unknown-top-level-field validator never learned about `publicProof`,
   so every manifest carrying it warned "Manifest contains unknown
   top-level field: publicProof." That was invisible on the first pass and
   appeared on every LATER one, because the first pass parses a manifest
   with no such key while later passes reload the persisted snapshot --
   which my loader change now serializes the field into. The warning lands
   in the published review comment, so an unchanged PR got a fresh comment
   PATCH on every regate sweep: exactly the #3379 churn that test exists to
   prevent, reintroduced by a field the writer knew about and the reader
   did not.

   Found by instrumenting the test's PATCH interception to diff the two
   comment bodies rather than guessing at the cause; the added line named
   itself.

2. config/examples/loopover.full.yml must mirror .loopover.yml.example
   from "WHERE IT LIVES" onward, and I documented the block in only one of
   the two.

Verified against origin/main first to confirm both were regressions from
this branch rather than pre-existing.

* test(proof): close the patch-coverage gaps, and fix a second sync-throw the gap exposed

Codecov flagged 8 uncovered changed lines across focus-manifest.ts and
routes.ts. I had measured coverage on proof-summary.ts and proof-badge.ts
only, and never on the two files the manifest block and the routes
actually touched -- so the gap was in my own verification, not just the
tests.

Closing it turned up a real defect rather than only missing assertions:
loadProofPageRepoOverride used `.catch()` on the injected manifest loader,
so a loader throwing SYNCHRONOUSLY (a driver-level failure before it ever
returns a promise) skipped the handler entirely and would have escaped to
the route -- 503ing a public page over a manifest read that is supposed to
be optional. That is the same defect this file already had in
loadProofSummary's section reads, which I fixed there and then
reintroduced here. Now a real try/catch, with a regression test using a
synchronously-throwing loader.

Coverage:
- parsePublicProofConfig / publicProofConfigToJson: explicit on/off, a
  present-but-empty block (present-but-false, which the resolver keys on),
  absence, three non-mapping shapes warning rather than throwing, and a
  snapshot round-trip.
- A regression test asserting publicProof is a KNOWN top-level field, so
  the writer/reader split behind the #3379 regate churn cannot return.
- The two route 503 arms are unreachable today (every inner read is
  individually fail-safe), so they are excluded with the house v8 pragma
  and a note on why they are kept: a future unguarded read should degrade
  to 503 rather than 500 on an unauthenticated public route. The badge arm
  uses ignore start/stop -- `next 2` miscounts across a multi-line comment
  and left the return uncovered.

All three changed files now report zero uncovered changed lines.

* refactor(proof): one shared resolver for both surfaces, and delete the unreachable arms

Replaces the coverage pragmas with the fix they were papering over.

The gate, the read and the outcome now live in ONE resolver
(resolveProofPage) that both handlers render. That is not tidiness: the
gate previously lived inline in both route bodies and exactly one of them
was wired to the per-repo opt-out, which is the defect review caught. A
shared resolver makes "the page and the badge agree about whether this
repo is published" true by construction instead of by two call sites
remembering the same thing.

With that in place the two 503 arms were provably unreachable, because
loadProofSummary is TOTAL -- every read is wrapped per section, so a
failing ledger/anchor/record read degrades to that section's honest
neutral state and the page still composes. Rather than excluding dead
branches from coverage, the outcome is gone from the type: ProofPageResult
is `ok | disabled`. A test asserts the totality directly -- every
dependency failing at once, including a DB binding that throws on property
access, still resolves to a rendered page in its neutral states.

Same treatment for buildProofAccuracy's `!interval` guard: wilsonInterval
returns null exactly when there are no trials, which IS the
nothing-decided case, so one reachable guard covers both reasons a rate is
unpublishable instead of a dead branch behind a pragma.

Net: no `v8 ignore` pragmas anywhere in the #9569 code, and zero
uncovered changed lines or branches across proof-summary.ts, routes.ts and
focus-manifest.ts.

---------

Co-authored-by: loopover-orb[bot] <296761690+loopover-orb[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants