Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions apps/loopover-ui/content/docs/federated-fleet-intelligence.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Federated fleet intelligence
description: Opt-in sharing of anonymized gate-calibration aggregates between self-hosted LoopOver instances — what is shared, how consent works, and how trust is gated.
---

Federated fleet intelligence is an **opt-in** path for self-hosted LoopOver (ORB) operators who want to compare their own gate calibration against peer medians. Nothing is shared unless you turn it on in config-as-code. There is no default collector and no automatic peer discovery.

This page describes the **shipped** export and transport behavior, plus the trust-gating design operators should verify for themselves. Receiving-side signature verification and allowlist import (#6480) continue to land separately; until that lands, treat inbound peer bundles as untrusted input you do not fold into local calibration automatically.

## What is shared (and what is not)

When enabled, this instance can build a **signed, anonymized calibration bundle** from its own local review outcomes. The bundle is an aggregate snapshot over a calibration window — not a stream of per-PR events.

Every field in the bundle body is aggregate-only. The export query never selects repo names, PR numbers, GitHub logins, commit SHAs, diffs, source code, or raw gate-reason text. The opaque `instanceId` is the same HMAC-derived handle the existing orb pipeline already uses — not a human identity.

Fields currently enumerated on the signed body (`FederatedSignalBundleBody`):

| Field | Meaning |
| --- | --- |
| `schemaVersion` | Bundle contract version (receivers can reject unknown versions) |
| `instanceId` | Opaque per-instance handle (no PII) |
| `generatedAt` | ISO timestamp the bundle was built |
| `windowDays` | Calibration window length (so peers only median equal-length windows) |
| `decided` | Resolved PRs in-window that the gate decided |
| `mergePrecision` | P(merged & not reverted \| gate said merge), or null until enough samples |
| `closePrecision` | P(closed & not reopened \| gate said close), or null until enough samples |
| `fpRate` / `fnRate` | False-positive / false-negative rates, or null until eligible |
| `reversalRate` | Share of decided PRs a human reversed |
| `cycleP50Ms` / `cycleP95Ms` | Gate-decision → resolution latency percentiles |
| `slopRate` | Share of decided PRs whose reason bucketed to `slop_advisory` |
| `copycatRate` | Share whose reason bucketed to `duplicate_risk` (aggregate rate only) |

A detached `signature` accompanies the body. Export today signs with a local HMAC key; the trust design (#6477) calls for **operator-allowlisted peer public keys** on the receive path so verification does not require sharing an anonymization secret.

This path is deliberately **not** the always-on orb telemetry export (`ORB_AIR_GAP` / central ingest). Federated export is config-as-code, peer- or operator-collector oriented, aggregate-only, and default OFF.

## How opt-in works

Consent is procedural and explicit: edit the loopover self-repo's `.loopover.yml` (config-as-code). There is no federated environment-variable kill-switch separate from this block — **absent or `enabled: false` means nothing is bundled and no federated network call is made**.

Minimal enable (export capability only — still no network push/pull without a collector):

<CodeBlock
lang="yaml"
code={`federatedIntelligence:
enabled: true`}
/>

To also arm the **transport client** (push this instance's bundle and/or pull peer bundles), set an operator-owned collector URL. There is deliberately **no default collector**:

<CodeBlock
lang="yaml"
code={`federatedIntelligence:
enabled: true
collectorUrl: "https://collector.example.com/v1/federated"
# optional: push | pull | both (default both when collectorUrl is set)
collectorMode: both`}
/>

`collectorUrl` must be a public HTTPS URL (the same SSRF-safe URL rules as other manifest URL fields). `enabled: true` alone still sends nothing over the wire if `collectorUrl` is unset.

## Trust-gating and poisoning resistance

The trust model (#6477) is intentional and checkable — not a reputation system:

1. **Explicit allowlist, not discovery.** Peer trust is operator-configured: you only consider bundles from sources you deliberately configured (a peer public key on your allowlist, or a collector URL you chose). There is no automatic peer discovery and no central PKI.
2. **Collector mode is the same rule.** Pointing at a collector *is* the trust decision. The client does not add a second peer-vetting layer on top of that choice.
3. **Median, not mean.** Peer comparisons use a **median** of peer calibration metrics. A median resists a bounded number of outlier/poisoned bundles; a mean does not.
4. **Sybil cost is on the receiver.** Flooding fake peers only matters if *you* added many fake keys to your own allowlist — that is self-limiting by construction.
5. **Abuse recovery.** If a peer looks wrong, remove that key (or stop pointing at that collector). v1 does not ship a separate retroactive-detection subsystem.

Until #6480's import/validation surface is live in your deployment, do not treat pulled files as automatically trusted calibration input.

## How to opt back out

Remove the block, or set `enabled: false`, on the loopover self-repo `.loopover.yml`, then redeploy/reload so the running instance rereads the manifest:

<CodeBlock
lang="yaml"
code={`federatedIntelligence:
enabled: false`}
/>

Clearing `collectorUrl` (or the whole block) also stops push/pull even if you leave other keys around. With federated intelligence off, behavior matches a deployment that never opted in: no federated bundle build for peers, no federated collector traffic.

## Related

- [Privacy & security](/docs/privacy-security) — hard public-surface rules
- [Self-host security](/docs/self-hosting-security) — securing a self-hosted review instance
- [Unified ORB + AMS](/docs/self-hosting-unified-ams-orb) — running review and miner together
5 changes: 5 additions & 0 deletions apps/loopover-ui/src/components/site/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const DEFAULT_ITEMS: PaletteItem[] = [
{ label: "Self-host backup and scaling", to: "/docs/self-hosting-backup-scaling", group: "Docs" },
{ label: "Self-host releases", to: "/docs/self-hosting-releases", group: "Docs" },
{ label: "Self-host security", to: "/docs/self-hosting-security", group: "Docs" },
{
label: "Federated fleet intelligence",
to: "/docs/federated-fleet-intelligence",
group: "Docs",
},
{ label: "Self-host troubleshooting", to: "/docs/self-hosting-troubleshooting", group: "Docs" },
{ label: "Branch analysis", to: "/docs/branch-analysis", group: "Docs" },
{ label: "Scoreability", to: "/docs/scoreability", group: "Docs" },
Expand Down
1 change: 1 addition & 0 deletions apps/loopover-ui/src/components/site/docs-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const docsNav: DocsGroup[] = [
{ to: "/docs/self-hosting-releases", label: "Releases & images" },
{ to: "/docs/self-hosting-release-checklist", label: "Release checklist" },
{ to: "/docs/self-hosting-security", label: "Security" },
{ to: "/docs/federated-fleet-intelligence", label: "Federated fleet intelligence" },
],
},
{
Expand Down
22 changes: 22 additions & 0 deletions apps/loopover-ui/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { Route as DocsLoopoverCommandsRouteImport } from './routes/docs.loopover
import { Route as DocsHowReviewsWorkRouteImport } from './routes/docs.how-reviews-work'
import { Route as DocsGithubAppRouteImport } from './routes/docs.github-app'
import { Route as DocsFumadocsSpikeApiReferenceRouteImport } from './routes/docs.fumadocs-spike-api-reference'
import { Route as DocsFederatedFleetIntelligenceRouteImport } from './routes/docs.federated-fleet-intelligence'
import { Route as DocsBranchAnalysisRouteImport } from './routes/docs.branch-analysis'
import { Route as DocsBetaOnboardingRouteImport } from './routes/docs.beta-onboarding'
import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.ams-unattended-scheduling'
Expand Down Expand Up @@ -328,6 +329,12 @@ const DocsFumadocsSpikeApiReferenceRoute =
path: '/fumadocs-spike-api-reference',
getParentRoute: () => DocsRoute,
} as any)
const DocsFederatedFleetIntelligenceRoute =
DocsFederatedFleetIntelligenceRouteImport.update({
id: '/federated-fleet-intelligence',
path: '/federated-fleet-intelligence',
getParentRoute: () => DocsRoute,
} as any)
const DocsBranchAnalysisRoute = DocsBranchAnalysisRouteImport.update({
id: '/branch-analysis',
path: '/branch-analysis',
Expand Down Expand Up @@ -509,6 +516,7 @@ export interface FileRoutesByFullPath {
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
'/docs/github-app': typeof DocsGithubAppRoute
'/docs/how-reviews-work': typeof DocsHowReviewsWorkRoute
Expand Down Expand Up @@ -582,6 +590,7 @@ export interface FileRoutesByTo {
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
'/docs/github-app': typeof DocsGithubAppRoute
'/docs/how-reviews-work': typeof DocsHowReviewsWorkRoute
Expand Down Expand Up @@ -659,6 +668,7 @@ export interface FileRoutesById {
'/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/federated-fleet-intelligence': typeof DocsFederatedFleetIntelligenceRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
'/docs/github-app': typeof DocsGithubAppRoute
'/docs/how-reviews-work': typeof DocsHowReviewsWorkRoute
Expand Down Expand Up @@ -737,6 +747,7 @@ export interface FileRouteTypes {
| '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/federated-fleet-intelligence'
| '/docs/fumadocs-spike-api-reference'
| '/docs/github-app'
| '/docs/how-reviews-work'
Expand Down Expand Up @@ -810,6 +821,7 @@ export interface FileRouteTypes {
| '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/federated-fleet-intelligence'
| '/docs/fumadocs-spike-api-reference'
| '/docs/github-app'
| '/docs/how-reviews-work'
Expand Down Expand Up @@ -886,6 +898,7 @@ export interface FileRouteTypes {
| '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/federated-fleet-intelligence'
| '/docs/fumadocs-spike-api-reference'
| '/docs/github-app'
| '/docs/how-reviews-work'
Expand Down Expand Up @@ -1263,6 +1276,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsFumadocsSpikeApiReferenceRouteImport
parentRoute: typeof DocsRoute
}
'/docs/federated-fleet-intelligence': {
id: '/docs/federated-fleet-intelligence'
path: '/federated-fleet-intelligence'
fullPath: '/docs/federated-fleet-intelligence'
preLoaderRoute: typeof DocsFederatedFleetIntelligenceRouteImport
parentRoute: typeof DocsRoute
}
'/docs/branch-analysis': {
id: '/docs/branch-analysis'
path: '/branch-analysis'
Expand Down Expand Up @@ -1524,6 +1544,7 @@ interface DocsRouteChildren {
DocsAmsUnattendedSchedulingRoute: typeof DocsAmsUnattendedSchedulingRoute
DocsBetaOnboardingRoute: typeof DocsBetaOnboardingRoute
DocsBranchAnalysisRoute: typeof DocsBranchAnalysisRoute
DocsFederatedFleetIntelligenceRoute: typeof DocsFederatedFleetIntelligenceRoute
DocsFumadocsSpikeApiReferenceRoute: typeof DocsFumadocsSpikeApiReferenceRoute
DocsGithubAppRoute: typeof DocsGithubAppRoute
DocsHowReviewsWorkRoute: typeof DocsHowReviewsWorkRoute
Expand Down Expand Up @@ -1574,6 +1595,7 @@ const DocsRouteChildren: DocsRouteChildren = {
DocsAmsUnattendedSchedulingRoute: DocsAmsUnattendedSchedulingRoute,
DocsBetaOnboardingRoute: DocsBetaOnboardingRoute,
DocsBranchAnalysisRoute: DocsBranchAnalysisRoute,
DocsFederatedFleetIntelligenceRoute: DocsFederatedFleetIntelligenceRoute,
DocsFumadocsSpikeApiReferenceRoute: DocsFumadocsSpikeApiReferenceRoute,
DocsGithubAppRoute: DocsGithubAppRoute,
DocsHowReviewsWorkRoute: DocsHowReviewsWorkRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createFileRoute, notFound } from "@tanstack/react-router";
import { Suspense } from "react";

import { DocsPage } from "@/components/site/docs-page";
import { docsClientLoader } from "@/lib/docs-client-loader";

// Rendered from content/docs/federated-fleet-intelligence.mdx via fumadocs-mdx's browser entry
// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
// for why the loader below resolves only a plain, serializable path string.
export const Route = createFileRoute("/docs/federated-fleet-intelligence")({
loader: async () => {
const { docsSource } = await import("@/lib/docs-source");
const page = docsSource.getPage(["federated-fleet-intelligence"]);
if (!page) throw notFound();
return { path: page.path, title: page.data.title, description: page.data.description };
},
head: () => ({
meta: [
{ title: "Federated fleet intelligence — LoopOver docs" },
{
name: "description",
content:
"Opt-in sharing of anonymized gate-calibration aggregates between self-hosted LoopOver instances: what is shared, how consent works, and how trust is gated.",
},
{ property: "og:title", content: "Federated fleet intelligence — LoopOver docs" },
{
property: "og:description",
content:
"Opt-in sharing of anonymized gate-calibration aggregates between self-hosted LoopOver instances: what is shared, how consent works, and how trust is gated.",
},
{ property: "og:url", content: "/docs/federated-fleet-intelligence" },
],
links: [{ rel: "canonical", href: "/docs/federated-fleet-intelligence" }],
}),
component: FederatedFleetIntelligence,
});

function FederatedFleetIntelligence() {
const { path, title, description } = Route.useLoaderData();
const Content = docsClientLoader.getComponent(path);
return (
<DocsPage eyebrow="Self-hosting" title={title} description={description}>
<Suspense fallback={<p className="text-token-sm text-muted-foreground">Loading…</p>}>
<Content />
</Suspense>
</DocsPage>
);
}
1 change: 1 addition & 0 deletions apps/loopover-ui/src/routes/docs.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const AUDIENCES: Audience[] = [
links: [
{ to: "/docs/maintainer-self-hosting", label: "Self-host reviews" },
{ to: "/docs/self-hosting-unified-ams-orb", label: "Unified ORB + AMS" },
{ to: "/docs/federated-fleet-intelligence", label: "Federated fleet intelligence" },
{ to: "/docs/ams-deployment", label: "AMS deployment guide" },
{ to: "/docs/ams-operations-runbook", label: "AMS operations runbook" },
{ to: "/docs/ams-observability", label: "Observing your miner" },
Expand Down
Loading