fix(engine): share governor-ledger's repo-segment guard with the three normalizers that skipped it - #9625
Conversation
…e normalizers that skipped it idea-intake's targetRepo regex, discovery-index-contract's normalizeRepoFullName, and discovery-soft-claim's identical copy all checked "exactly one slash, non-empty halves" but skipped the "."/".." path-safety segment guard from the JSONbored#5831 -> JSONbored#7525 -> JSONbored#8350 fix family, so "../evilrepo" passed renter intake (reachable over HTTP), survived discovery-index candidate normalization, and could be built into a soft-claim request. Lift governor-ledger's guard verbatim into a new shared packages/loopover-engine/src/repo-segment.ts (REPO_SEGMENT_PATTERN + isValidRepoSegment, re-exported from the engine barrel), migrate governor-ledger off its private copy, and apply the repo-clone.ts split-and-guard shape at all three call sites: idea-intake rejects with the existing target_repo_malformed code, and both discovery normalizers return null so a rejected candidate is dropped with the existing invalid-candidate warning. No new error codes, warnings, or regexes; the miner package's own copy stays untouched. Regression tests land in BOTH graded suites: the engine package's own node:test suite (the `engine` Codecov flag's coverage run) and the root vitest unit suite (the `backend` flag), so every changed line is covered under each flag's own lcov. Closes JSONbored#9610
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 01:25:55 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed 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. 🟩 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. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9625 +/- ##
==========================================
+ Coverage 90.02% 90.08% +0.05%
==========================================
Files 888 889 +1
Lines 111983 112001 +18
Branches 26570 26586 +16
==========================================
+ Hits 100810 100891 +81
+ Misses 9843 9776 -67
- Partials 1330 1334 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Closes #9610
Summary
fix(engine): share governor-ledger's repo-segment guard with the three normalizers that skipped itThree
owner/reponormalizers inpackages/loopover-engineperformed the same "exactly one slash, both halves non-empty" check but skipped the"."/".."path-safety segment guard that closed #5831 → #7525 → #8350:idea-intake.ts—validateIdeaSubmission({ targetRepo: "../evilrepo" })returnedok: true(regex has the right character class but no traversal rejection), andbuildClaimPlancopied that value verbatim into everyClaimStep.targetRepo. This is the renter-supplied freeform intake path reachable over HTTP (POST /v1/loop/intake-idea,POST /v1/loop/plan-idea-claims), whose zod schema is deliberately loose because the engine validator "owns the real bounds/format checks".discovery-index-contract.ts:normalizeRepoFullName— data from the "OPTIONAL, only-partially-trusted hosted index" flowed intoowner/reposlicing with no segment guard.discovery-soft-claim.ts:normalizeRepoFullName— an identical unchecked copy whose JSDoc claimed it "mirrors the … claim-ledger repo validation" (which does callisValidRepoSegment).Root cause
The engine-side copy of the guard lived privately inside
governor-ledger.ts(deliberately not importable from the miner package), so each sibling normalizer hand-rolled its own slash check without the traversal rejection.Fix
packages/loopover-engine/src/repo-segment.tsexporting exactly two symbols —REPO_SEGMENT_PATTERN(/^[A-Za-z0-9._-]+$/) andisValidRepoSegment— a straight lift ofgovernor-ledger.ts:43-47, byte-for-byte semantics. Both re-exported frompackages/loopover-engine/src/index.tsin the existing explicit named-export barrel style.governor-ledger.tsdeletes its local copy and imports from./repo-segment.js; externally observable behaviour unchanged (its suite passes unmodified).idea-intake.tstargetRepostring branch now followsrepo-clone.ts:87-93's shape — split on/, reject empty/extra segments, thenisValidRepoSegmenton both halves — rejecting with the existingtarget_repo_malformedcode. No new error code.discovery-index-contract.tsnormalizeRepoFullNamereturnsnullwhen either segment fails the guard; a rejected candidate is still dropped with the existing"DiscoveryIndexResponse dropped an invalid or boundary-violating candidate."warning. No new warning string.discovery-soft-claim.tsnormalizeRepoFullNamelikewise returnsnull, sobuildSoftClaimRequestreturnsnullfor such a claim, exactly as it already does for a slashless value.packages/loopover-miner/lib/repo-clone.tsuntouched (engine must not import from the miner package). No widened character class, no new regex, no new error/warning strings.Tests (RED → GREEN)
New named regression tests, run against the unfixed source first:
(the traversal-rejection tests fail pre-fix; the round-trip/still-accepts tests pass, showing no behavior regression). After the fix, including the untouched governor-ledger suite and both intake routes suites:
validateIdeaSubmission({ …, targetRepo: "../evilrepo" })→{ ok: false, errors: ["target_repo_malformed"] };"./x","a/..","../.."covered in the same block;"acme/widgets"still resolves to{ kind: "existing", repo: "acme/widgets" }.normalizeDiscoveryIndexCandidate({ repoFullName: "../evil", issueNumber: 1, title: "x" })→null;normalizeDiscoveryIndexResponsedrops that candidate and emits the existing invalid-candidate warning.buildSoftClaimRequest({ …, repoFullName: "../evil" })→null."owner/repo"round-trips unchanged through all three normalizers, asserted in each suite.test/unit/repo-segment.test.tscovers the barrel re-export plus a passing slug, a pattern-failing segment,".", and"..".packages/loopover-engine/test/repo-segment.test.tsmirrors all four rejection classes and both accept paths inside the engine package's own node:test suite (801 tests, 801 pass), so the change is behavior-tested by the suite that actually grades engine source — not only by a vitest twin.Coverage
Engine source is graded under two Codecov flags, and this diff is covered under both flags' own coverage runs:
engineflag (c8 over the engine package's own node:test suite —node --experimental-strip-types scripts/engine-coverage.ts, CI's exact recipe): cross-referencing the producedpackages/loopover-engine/coverage/lcov.infoagainst this PR's diff hunks — every changed line covered, zero partial branches on changed lines, and the newrepo-segment.tsat 100% lines/branches:backendflag (root vitest v8 coverage,packages/loopover-engine/src/**/*.tsis insidecoverage.include): same cross-reference against the vitest lcov — all changed lines covered, no partial branches on changed lines;repo-segment.tsandidea-intake.tsat 100% stmts/branch/funcs/lines.Both arms of every new conditional are exercised:
isValidRepoSegmentpass / pattern-fail /"."/"..", and each of the three call sites with accepted and rejected values (empty halves, extra segments, non-slug characters, traversal segments).Validation commands (repo root)