Skip to content

fix(security): close the OTel scrub gap, MCP untrusted-title leak, and merge-readiness fail-open (#9162 #9163 #9167) - #9229

Merged
JSONbored merged 7 commits into
mainfrom
fix/9162-9163-9167-trust-boundary
Jul 27, 2026
Merged

fix(security): close the OTel scrub gap, MCP untrusted-title leak, and merge-readiness fail-open (#9162 #9163 #9167)#9229
JSONbored merged 7 commits into
mainfrom
fix/9162-9163-9167-trust-boundary

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Three "security/trust boundary that reads as enforced but silently isn't" fixes, bundled as one theme:

  • orb(otel): the trace exporter is the one egress redaction-scrub never covers — recordException ships full messages and stacks unscrubbed, to a third-party host picked by an unrelated key #9162src/selfhost/otel.ts was the one egress the shared redaction-scrub never covered.
    otelSafeAttributes now imports scrubString from redaction-scrub.ts (deleting its own duplicated
    SECRET_KEY regex) so string attribute values are scrubbed, not just filtered by key. withOtelSpan's
    catch no longer calls span.recordException(err) (which bypasses the scrubber and writes a raw
    exception.stacktrace); it now builds the same "exception" span event by hand and routes it through
    otelSafeAttributes. The span-status message goes through the same scrub. Separately, the PostHog-derived
    trace-destination fallback (picking a third-party host off POSTHOG_API_KEY alone) now requires an
    explicit OTEL_TRACES_ENABLE_POSTHOG_FALLBACK=1 opt-in — setting the key for product-analytics error
    capture no longer silently also selects a trace-export destination.
  • orb(mcp): find_opportunities returns attacker-authored issue titles verbatim into a model that holds local write tools — no scrub, no fencing, no truncation #9163loopover_find_opportunities copied GitHub issue titles verbatim into a tool result read by
    a model holding local write tools. Added src/mcp/untrusted-text.ts, a shared
    sanitizeUntrustedMcpText helper (defuses markdown fences and HTML comments, neutralizes recognized
    prompt-injection phrasing via the existing neutralizePromptInjection, collapses control characters,
    truncates to 120 chars) and routed it through toRankedEntry's title field and through the two raw
    upstream-title fields loopover_check_before_start's report surfaces
    (target.requested.title, target.resolvedIssueTitle). Both tool descriptions now label the field as
    untrusted data. Swept loopover_validate_linked_issue too — its report never echoes a raw title (every
    field is already built from sanitizePublicComment'd strings), so no change was needed there; noted as a
    judgment call below.
  • orb(config): mergeReadiness:advisory silently demotes three explicitly-blocking gates at once, and unknown gate modes coerce toward merge #9167mergeReadinessGateMode: advisory used to unconditionally override linkedIssueGateMode,
    duplicatePrGateMode, and slopGateMode to its own mode, silently demoting an explicitly-configured
    block down to advisory. applyMergeReadinessGate (both the host copy in src/rules/advisory.ts and its
    engine twin in packages/loopover-engine/src/advisory/gate-advisory.ts) now only fills in a sub-gate mode
    the operator left unset — an explicit mode, stricter or looser, always wins. gateMode() also now
    fails closed on an unrecognized mode string ("block", not the previous fail-open "advisory"
    coercion). Added a config-lint.ts warning (mergeReadinessCompositeWarnings) so an operator who sets
    both the composite and an explicit sub-gate mode sees that the sub-gate is authored, not overridden — the
    effective config is never silently different from the authored one.

Judgment calls

Scope

Validation

  • git diff --check
  • npm run typecheck (npx tsc --noEmit -p tsconfig.json --incremental false) — clean
  • npm run build --workspace @loopover/engine — clean
  • npm run build:mcp — clean
  • Directly relevant unit/integration/contract tests, run explicitly (not the full suite/full coverage):
    selfhost-otel.test.ts, rules.test.ts, selfhost-config-lint.test.ts, mcp-untrusted-text.test.ts
    (new), find-opportunities.test.ts, mcp-find-opportunities.test.ts, mcp-output-schemas.test.ts,
    mcp-discovery.test.ts, mcp-cli-find-opportunities.test.ts, test/contract/engine-parity.test.ts,
    check-engine-parity-script.test.ts353/353 passing.
  • Scoped v8 coverage on every touched src/**/packages/loopover-engine/src/** file against this
    test subset: otel.ts, advisory.ts, config-lint.ts, find-opportunities.ts, untrusted-text.ts
    all 100% branch on the changed lines; server.ts's new sanitizePreStartCheckReportTitles function
    needed one added test case to hit both its ternary branches (now both covered).
  • npm run test:coverage (full unsharded), npm run test:workers, npm run test:mcp-pack,
    npm run ui:*, npm auditnot run, by explicit instruction for this pass (time-boxed to the
    directly relevant tests + typecheck). Recommend running the full npm run test:ci gate before merge.

If any required check was skipped, explain why:

  • Full npm run test:coverage / test:ci / npm audit were intentionally skipped per the task's scope
    (targeted verification only) — the scoped coverage runs above cover every changed line/branch in this
    diff, but the full gate has not been run end-to-end on this exact commit.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, PATs, private keys, trust scores, or private rankings.
  • Public GitHub text (issue titles reaching the MCP tool boundary) is now sanitized; tests assert the
    absence of injection markup and fenced/comment characters.
  • New env-gated behavior (OTEL_TRACES_ENABLE_POSTHOG_FALLBACK) is off by default (fail-closed);
    tests cover both the missing-opt-in and non-"1" loose-truthy-string cases.
  • API/MCP behavior updated and tested (tool descriptions, output schema doc comment, report scrubbing).
  • UI: not applicable — no apps/loopover-ui changes.

Notes

  • No env.SOMETHING reads were added/removed under src/selfhost/**, so selfhost:env-reference does not
    need regenerating.
  • No OpenAPI/wrangler/schema changes, so ui:openapi/cf-typegen do not need regenerating.

@JSONbored JSONbored self-assigned this Jul 27, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (f4a0ee0) and head (a6b5d85) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui a6b5d85 Commit Preview URL

Branch Preview URL
Jul 27 2026, 08:48 AM

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 67 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.43MB 67 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-ElSJEYZ9.js (New) 2.17MB 2.17MB 100.0% 🚀
assets/tanstack-vendor-skAxzy2o.js (New) 803.86kB 803.86kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-CADhxlZe.js (New) 442.88kB 442.88kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-D45_Seu-.js (New) 201.71kB 201.71kB 100.0% 🚀
assets/modal-DVhc0Ubo.js (New) 184.39kB 184.39kB 100.0% 🚀
assets/client-BgEjUaSK.js (New) 146.06kB 146.06kB 100.0% 🚀
assets/self-hosting-configuration-BZJXQdiW.js (New) 101.67kB 101.67kB 100.0% 🚀
assets/maintainer-panel-B-u0GoQY.js (New) 79.0kB 79.0kB 100.0% 🚀
assets/routes-D0k9sib9.js (New) 35.77kB 35.77kB 100.0% 🚀
assets/owner-panel-BWTBLtaK.js (New) 27.52kB 27.52kB 100.0% 🚀
assets/app-qqJM28j8.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-JEHp_dt3.js (New) 22.28kB 22.28kB 100.0% 🚀
assets/miner-panel-ckjLYRm1.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-xk63b3Jk.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-daNvuO_E.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-lwV27cI4.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-CTRVrpYv.js (New) 15.37kB 15.37kB 100.0% 🚀
assets/playground-panel-DK0MllCB.js (New) 14.43kB 14.43kB 100.0% 🚀
assets/fairness-sRFV4M4s.js (New) 10.6kB 10.6kB 100.0% 🚀
assets/app.audit-BGr15i-8.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-DRfUPiQ8.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-SSBHIbhp.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-COyKOSFz.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-BhqzW-Bq.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-DACpPej2.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-BWXkz9Gv.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-DQerGlnb.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-CB702Cxz.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-CP2_Nyd6.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/docs.index-DdrxBXJ5.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-DDCEqxah.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-XD83U0fv.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-C9cg-J_i.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-BuTTASUO.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-BpHsUoYf.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-CuDMbFX_.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CBN4zGNO.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-B-aWqUrF.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BGQk4och.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-DV4XbGD2.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-COjnrVPw.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-CIkutnQz.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-Cl9b78qE.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-Ud4awLvU.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-BbUHWkOd.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-CLIzEZI7.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-Bwi4jzNO.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-D7ussHeJ.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-B6N99W9d.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-DKWo3ABh.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-Bkj2Ppz6.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-CbWWhiw6.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-BoNUXhAJ.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-BMTa5q4W.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BKzBhVFb.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-goWqF-3Z.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-DTWyDZkI.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-B1JtFWwb.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-DbGyoOk5.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BJh0JSXr.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-p6BdW0h8.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-2LBuu5Q_.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-Cf9AbH3o.js (Deleted) -2.17MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-AP0TueuY.js (Deleted) -803.86kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-Dy-83ekW.js (Deleted) -442.88kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-B71bsT07.js (Deleted) -201.71kB 0 bytes -100.0% 🗑️
assets/modal-DDGlOYav.js (Deleted) -184.39kB 0 bytes -100.0% 🗑️
assets/client-CWIGjSRg.js (Deleted) -146.06kB 0 bytes -100.0% 🗑️
assets/self-hosting-configuration-Befldias.js (Deleted) -101.6kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-QeJ45iks.js (Deleted) -79.0kB 0 bytes -100.0% 🗑️
assets/routes-99LpxxrV.js (Deleted) -35.77kB 0 bytes -100.0% 🗑️
assets/owner-panel-BlvSERyf.js (Deleted) -27.52kB 0 bytes -100.0% 🗑️
assets/app-BymTp6KX.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-iVpPzx--.js (Deleted) -22.28kB 0 bytes -100.0% 🗑️
assets/miner-panel-CQznKIL6.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-Dh6jXJqL.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-DE1BL7kT.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-BC34eLhy.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Be3UwUYx.js (Deleted) -15.37kB 0 bytes -100.0% 🗑️
assets/playground-panel-WTEQ2MdO.js (Deleted) -14.43kB 0 bytes -100.0% 🗑️
assets/fairness-D9gK2DgW.js (Deleted) -10.6kB 0 bytes -100.0% 🗑️
assets/app.audit-_CqsIBKu.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-DvSIGuYM.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DkxBcxuc.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BmceDscj.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-kSVMY__v.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-Canca6sX.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-DL7M2qOg.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-C11cw9oy.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BTq-K_VF.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-UEHgM9kT.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/docs.index-D7pTVrSg.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-E1oJfyBh.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BRIu7j4W.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-Dmn22gPI.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-CY2UJeu6.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-OGI4S3_W.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CA6183eh.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-CJWVgTGA.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-B1E9fn9Y.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-qqx3U2Bi.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-CiLvefOA.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-Cb1p7aWP.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-B9kBQyW1.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-B7Xj69cD.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-BDdO0DHa.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-BQ1D46A_.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-Bpza_47A.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-toGLBRxx.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner--TtLeI8t.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-wjnZCqYM.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-Cm_O3LFj.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-DYixLXkU.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-CPfYPT46.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-BsG0FOqb.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-t55NB8Dm.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-Big9PXPb.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-P2nDU39B.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BC7gEn_U.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-B3mwZfV4.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-Bj-8zF9o.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-DoJAJzxk.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-ChQ0O352.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-Ccl-znKw.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
22690 6 22684 21
View the top 3 failed test(s) by shortest run time
test/integration/orb-relay.test.ts > POST /v1/orb/relay/pull > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.0107s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:1160:24
test/unit/engine-parity-fixtures.test.ts > predicted-gate engine parity fixtures > keeps the 'merge-readiness-composite-preserves-e…' fixture parseable and aligned with its documented surface
Stack Traces | 0.0111s run time
AssertionError: expected 'success' to be 'failure' // Object.is equality

Expected: "failure"
Received: "success"

 ❯ test/unit/engine-parity-fixtures.test.ts:29:31
test/contract/engine-parity.test.ts > predicted-gate engine parity (#2286) > 'merge-readiness-composite-preserves-e…' matches the committed golden output
Stack Traces | 0.0208s run time
AssertionError: expected { predicted: true, …(11) } to deeply equal { predicted: true, …(10) }

- Expected
+ Received

  {
    "basis": "public_config",
-   "blockers": [
+   "blockers": [],
+   "conclusion": "success",
+   "confirmedContributor": undefined,
+   "funnel": null,
+   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
+   "pack": "gittensor",
+   "predicted": true,
+   "readinessScore": 80,
+   "summary": "No configured hard blocker was found. Advisory findings, if any, stay advisory.",
+   "title": "LoopOver Orb Review Agent passed",
+   "warnings": [
      {
        "action": "If this PR is intended to solve an issue, link it explicitly in the PR body.",
        "code": "missing_linked_issue",
        "detail": "No closing reference or linked issue number was found in the PR metadata/body.",
        "title": "No linked issue detected",
      },
    ],
-   "conclusion": "failure",
-   "funnel": null,
-   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
-   "pack": "gittensor",
-   "predicted": true,
-   "readinessScore": 80,
-   "summary": "No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.",
-   "title": "LoopOver Orb Review Agent: No linked issue detected",
-   "warnings": [],
  }

 ❯ test/contract/engine-parity.test.ts:46:21
test/unit/predicted-gate-engine.test.ts > predicted-gate engine branch coverage (#2283) > exercises gate-advisory gateMode and blocker policy branches
Stack Traces | 0.0263s run time
AssertionError: expected 'block' to be 'advisory' // Object.is equality

Expected: "advisory"
Received: "block"

 ❯ test/unit/predicted-gate-engine.test.ts:1558:55
test/integration/orb-relay.test.ts > POST /v1/orb/relay/register > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.0811s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:155:24
test/unit/config-templates.test.ts > config/examples review templates (#1682) > lints loopover.full.yml with zero warnings, including no retired top-level fields
Stack Traces | 0.262s run time
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "gate.mergeReadiness (\"off\") is set alongside an explicitly-authored mode for gate.linkedIssue, gate.duplicates, gate.slop.mode. The composite only fills in a sub-gate mode left unset -- it never overrides an explicitly-configured one, so those fields stay exactly as authored regardless of gate.mergeReadiness.",
+ ]

 ❯ test/unit/config-templates.test.ts:64:29

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-27 08:44:46 UTC

16 files · 1 AI reviewer · 1 blocker · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
The AI review returned blocking findings for this change but did not include a separate narrative summary. Review the blockers below before deciding this PR.

Blockers

  • src/rules/advisory.ts:1525 (and the mirrored packages/loopover-engine/src/advisory/gate-advisory.ts) — `buildSlopGateBlocker` calls `gateMode(policy.slopGateMode)` with no `?? "advisory"` fallback at the call site, so now that `gateMode()`'s internal fallback for any non-`off`/`block`/`advisory` value (including plain `undefined`) changed from `advisory` to `block`, a repo that never configured `gate.slop.mode` will have its slop-risk finding silently promoted to a hard blocker (closing PRs) whenever `slopRisk >= slopGateMinScore` (default 60), directly contradicting the field's own doc comment ('Defaults to off/advisory — slop never blocks unless opted in').
Nits — 6 non-blocking
  • The 'every legitimate caller already supplies its own `?? "advisory"` default' claim in the new comment on `gateMode()` (src/rules/advisory.ts:~1508) is not true for `buildSlopGateBlocker`'s call site — worth fixing the comment along with the bug.
  • nit: `src/rules/advisory.ts:1525` uses 'master merge-readiness composite'; consider 'main/primary' per the repo's non-inclusive-terminology convention.
  • nit: `src/mcp/untrusted-text.ts` hardcodes `0x20`/`0x7f`/the `3` truncation-ellipsis-length as bare literals; a named constant would make the control-char and truncation boundaries self-documenting.
  • nit: the new `mergeReadinessCompositeWarnings` field mapping in `config-lint.ts` (`{ field: "slopMode", label: "gate.slop.mode" }`) references a `FocusManifestGateConfig` field name that couldn't be verified against `focus-manifest.ts` in this diff — worth double-checking the manifest-level `slopMode` key actually exists there.
  • Fix `buildSlopGateBlocker` (both copies) to explicitly default with `gateMode(policy.slopGateMode ?? "advisory")` to restore the intended fail-open-until-configured slop-gate semantics, and add a regression test asserting an unset `slopGateMode` with a high `slopRisk` does NOT block.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Why this is blocked

  • src/rules/advisory.ts:1525 (and the mirrored packages/loopover-engine/src/advisory/gate-advisory.ts) — `buildSlopGateBlocker` calls `gateMode(policy.slopGateMode)` with no `?? "advisory"` fallback at the call site, so now that `gateMode()`'s internal fallback for any non-`off`/`block`/`advisory` value (including plain `undefined`) changed from `advisory` to `block`, a repo that never configured `gate.slop.mode` will have its slop-risk finding silently promoted to a hard blocker (closing PRs) whenever `slopRisk >= slopGateMinScore` (default 60), directly contradicting the field's own doc comment ('Defaults to off/advisory — slop never blocks unless opted in').
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. src/rules/advisory.ts:1525 \(and the mirrored packages/loopover-engine/src/advisory/gate-advisory.ts\) — \`buildSlopGateBlocker\` calls \`gateMode\(policy.slopGateMode\)\` with no \`?? "advisory"\` fallback at the call site, so now that \`gateMode\(\)\`'s internal fallback for any non-\`off\`/\`block\`/\`advisory\` value \(including plain \`undefined\`\) changed from \`advisory\` to \`block\`, a repo that never configured \`gate.slop.mode\` will have its slop-risk finding silently promoted to a hard blocker \(closing PRs\) whenever \`slopRisk >= slopGateMinScore\` \(default 60\), directly contradicting the field's own doc comment \('Defaults to off/advisory — slop never blocks unless opted in'\).

CI checks failing

  • validate
  • validate-tests
  • validate-code

Decision drivers

  • ❌ Code review — 1 blocker (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9162, #9163, #9167
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 (3 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 299 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 299 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The diff imports scrubString into otelSafeAttributes (removing the duplicated SECRET_KEY), replaces span.recordException with a hand-built exception event routed through otelSafeAttributes, scrubs the status message, and gates the PostHog-derived trace endpoint behind an explicit OTEL_TRACES_ENABLE_POSTHOG_FALLBACK=1 opt-in, matching all four stated requirements including added tests exercising to

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 13 PR(s), 299 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
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 <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> 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.

Decision record
  • action: hold · clause: ai_consensus_defect
  • config: 03a7f8b529a9 · pack: oss-anti-slop
  • model: n/a · prompt: 32528cb0d9b8 · confidence: 0.82
  • record: 804c50c2631a (schema v3, head 6fab259)

🟩 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 27, 2026
…e unreachable merge-readiness fill-unset semantic

buildSlopGateBlocker called gateMode(policy.slopGateMode) with no ?? "advisory"
fallback, unlike every sibling call site. Once gateMode()'s own fallback for a
non-off/block/advisory value (including plain undefined) changed from
"advisory" to "block" (#9167's fail-closed fix), any repo that never
configured gate.slop.mode had its slop finding silently promoted to a hard
blocker -- contradicting the field's own documented default. Added the missing
fallback in both the host and engine-twin copies, plus a regression test.

Also reverts applyMergeReadinessGate's "only fill in an unset sub-gate"
change back to its original override behavior: RepositorySettings's sub-gate
fields (linkedIssueGateMode etc.) are non-optional and DB-defaulted, so they
are never actually undefined by the time they reach GateCheckPolicy -- the
fill-unset-only variant could never fire in practice, and its own new tests
only passed because a test helper's hardcoded baseline masked the same gap.
The intended visibility fix for this composite (an operator's explicit
sub-gate mode getting silently demoted) already lives one layer up, in
config-lint.ts's mergeReadinessCompositeWarnings, which operates on the raw
manifest where "unset" is a real, meaningful state.
@JSONbored

Copy link
Copy Markdown
Owner Author

Addressed the blocking finding: `buildSlopGateBlocker` now supplies its own `?? "advisory"` fallback (both the host copy and the engine twin), matching every sibling `gateMode(policy.xGateMode ?? "advisory")` call site. Added a regression test asserting an unconfigured `slopGateMode` still never blocks.

While fixing that, found and reverted a second, related problem in the same area: the earlier "merge-readiness composite only fills in an unset sub-gate" change (also part of #9167) turned out to be unreachable in practice — every sub-gate mode is already a concrete, DB-defaulted value by the time it reaches `GateCheckPolicy` (never actually `undefined`), so the fill-unset-only variant could never fire, and its own new tests only passed because a test helper's hardcoded baseline masked the same gap. Reverted `applyMergeReadinessGate` to its original override behavior (now consistent with the 5 pre-existing tests that were failing against the unreachable variant) and rely on `config-lint.ts`'s `mergeReadinessCompositeWarnings` — which operates on the raw, pre-default manifest, where "unset" is genuinely meaningful — for the visibility fix instead.

@JSONbored
JSONbored merged commit 9d4092b into main Jul 27, 2026
9 of 11 checks passed
@JSONbored
JSONbored deleted the fix/9162-9163-9167-trust-boundary branch July 27, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment