Skip to content

fix(discovery-index): cap the untrusted response candidate list (#6774) - #6870

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
real-venus:fix/discovery-index-response-cap-6774
Jul 17, 2026
Merged

fix(discovery-index): cap the untrusted response candidate list (#6774)#6870
JSONbored merged 1 commit into
JSONbored:mainfrom
real-venus:fix/discovery-index-response-cap-6774

Conversation

@real-venus

Copy link
Copy Markdown
Contributor

Summary

normalizeDiscoveryIndexResponse (packages/loopover-engine/src/discovery-index-contract.ts) looped over the
response's candidates array with no length cap. The request side already clamps page size to
MAX_PAGE_LIMIT (200) via clampLimit, but the response comes from the OPTIONAL, only-partially-trusted
hosted discovery-index service
— this module's own header frames it as an external boundary
("NO raw scores / rewards / wallet / hotkey data / source contents crossing the public boundary"). A misbehaving
or compromised host could return an arbitrarily large candidates array and force unbounded client-side
normalization.

This caps the array at MAX_PAGE_LIMIT, dropping the overflow with a warning:

const boundedCandidates = rawCandidates.length > MAX_PAGE_LIMIT ? rawCandidates.slice(0, MAX_PAGE_LIMIT) : rawCandidates;
if (boundedCandidates.length < rawCandidates.length) {
  warnings.push(`DiscoveryIndexResponse returned ${rawCandidates.length} candidates; capping to ${MAX_PAGE_LIMIT} and dropping the rest.`);
}

The retained page and nextCursor still round-trip, so forward pagination continues from the truncated page.
Consistent with the module's tolerant-parser convention (degrade with a warning, never throw).

Closes #6774

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • 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 a currently open issue this PR resolves (Closes #6774).

Validation

  • git diff --check
  • npm run typecheck
  • npm run test:coverage locally — the new cap lines are exercised on both branches (oversized → truncate + warn; the ≤ cap path is covered by the existing response test), verified via coverage-final.json.
  • npx vitest run test/unit/discovery-index-contract.test.ts (17 passing, incl. the new 250→200 truncation regression asserting the warning + preserved nextCursor)
  • engine build + node --test (588 passing) + engine-parity:drift-check

If any required check was skipped, explain why:

  • Single engine parser change + its unit test; no UI/OpenAPI/MCP/migration surface, so those checks are N/A to this 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. — N/A: no auth/session surface; this hardens an untrusted-input parse path with a test.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A: no API/MCP surface changed (schema/shape module only, no endpoint).
  • No visible UI changes (backend parser only), so no UI Evidence section is required.
  • Public docs/changelogs: none needed.

Notes

  • The cap reuses the existing MAX_PAGE_LIMIT (200) — a well-behaved host can never return more than one page's worth, so this only ever drops data an over-limit/hostile host tried to push. nextCursor is parsed independently of the candidate list, so truncating a page does not break pagination.

…bored#6774)

normalizeDiscoveryIndexResponse looped over the response's `candidates` array with no length cap. The
request side already clamps page size to MAX_PAGE_LIMIT (200), but the response comes from the OPTIONAL,
only-partially-trusted hosted discovery-index service (this module's own header frames it as an external
boundary). A misbehaving or compromised host could return an arbitrarily large `candidates` array and
force unbounded client-side normalization.

Caps the array at MAX_PAGE_LIMIT, dropping the overflow with a warning; the retained page and `nextCursor`
still round-trip so forward pagination continues from the truncated page. Adds a regression test asserting
an oversized (250-candidate) response is truncated to 200 with the warning and a preserved cursor.

Closes JSONbored#6774
@real-venus
real-venus requested a review from JSONbored as a code owner July 17, 2026 09: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 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.65%. Comparing base (d2244e9) to head (ca5c81a).
⚠️ Report is 22 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6870   +/-   ##
=======================================
  Coverage   93.65%   93.65%           
=======================================
  Files         685      685           
  Lines       68247    68250    +3     
  Branches    18707    18709    +2     
=======================================
+ Hits        63914    63917    +3     
  Misses       3348     3348           
  Partials      985      985           
Flag Coverage Δ
shard-1 43.63% <100.00%> (-0.05%) ⬇️
shard-2 36.80% <0.00%> (+0.12%) ⬆️
shard-3 32.80% <0.00%> (+0.09%) ⬆️
shard-4 34.83% <0.00%> (+0.06%) ⬆️
shard-5 31.86% <0.00%> (+0.70%) ⬆️
shard-6 45.39% <0.00%> (-0.31%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...es/loopover-engine/src/discovery-index-contract.ts 100.00% <100.00%> (ø)

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

loopover-orb Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-17 10:04:41 UTC

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

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This adds a straightforward cap on the untrusted discovery-index response's `candidates` array at MAX_PAGE_LIMIT (200), dropping overflow entries with a warning while preserving `nextCursor` for pagination continuity. The fix is correctly placed at the boundary where untrusted data enters (before the per-candidate normalization loop), closes issue #6774 as described, and is backed by a real test that exercises both branches (oversized truncation and the existing ≤cap path). This is a small, well-targeted, defensive fix consistent with the module's tolerant-parser convention.

Nits — 4 non-blocking
  • The slice at discovery-index-contract.ts creates a new array even when unbounded; for the common case this is a harmless allocation but could be avoided by only slicing when the length check triggers (it already does this correctly, so this is just a style observation, not a real issue).
  • The warning message hardcodes the cap value via template literal (`MAX_PAGE_LIMIT`); consider verifying no downstream code parses these warning strings by exact match, since count-specific warnings can shift if the cap changes.
  • Consider whether truncation should happen before or after boundary-violation filtering — currently it truncates raw entries first, so a batch of 250 candidates where the last 50 are all invalid would still process the first 200 (which may include valid ones), which seems like the correct behavior but is worth a one-line comment confirming intent.
  • The test at discovery-index-contract.test.ts:180 is well-scoped; consider also asserting `parsed.response.candidates[0]?.issueNumber` to confirm the *first* 200 (not a random subset) are retained, making the ordering guarantee explicit.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #6774
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 156 registered-repo PR(s), 85 merged, 20 issue(s).
Contributor context ✅ Confirmed Gittensor contributor real-venus; Gittensor profile; 156 PR(s), 20 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Review context
  • Author: real-venus
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: TypeScript, JavaScript, Python, Rust, CSS, MDX, Svelte, Swift
  • Official Gittensor activity: 156 PR(s), 20 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Add a concise scope and risk note.
  • Then work through the remaining 1 step in the Signals table above.
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.
🧪 Chat with LoopOver

Ask 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.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 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.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 17, 2026
@JSONbored
JSONbored merged commit ff46459 into JSONbored:main Jul 17, 2026
16 checks passed
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.

discovery-index-contract.ts caps every request-side list but leaves the response candidate list unbounded

2 participants