Skip to content
Merged
14 changes: 14 additions & 0 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -1493,3 +1493,17 @@ settings:
# # To stop trusting a peer, remove its key -- that is the whole revocation story, by design.
# peerKeys:
# - 0000000000000000000000000000000000000000000000000000000000000000

# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page
# (`/proof/<owner>/<repo>`) and its README badge.
#
# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the
# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already
# curl would add friction without adding privacy. This block exists because a page is nonetheless a
# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers.
#
# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn
# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely
# to keep the page on once the operator enables it.
# publicProof:
# enabled: false # Bool. Default when the block is absent: true (opt-out).
76 changes: 76 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -26299,6 +26299,82 @@
}
}
}
},
"/v1/public/repos/{owner}/{repo}/proof": {
"get": {
"operationId": "getPublicRepoProof",
"tags": [
"Public"
],
"summary": "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "owner",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": true,
"name": "repo",
"in": "path"
}
],
"responses": {
"200": {
"description": "ProofSummary. Any accuracy figure carries its coverage AND a Wilson confidence interval; below the sample floor it is an explicit `insufficient_data` state, never a bare percentage. Carries the verification-boundary statement in the payload"
},
"404": {
"description": "The proof page is disabled fleet-wide, or this repo has opted out"
},
"503": {
"description": "Composition failed — no partial or fabricated summary is served"
}
}
}
},
"/v1/public/repos/{owner}/{repo}/proof-badge.svg": {
"get": {
"operationId": "getPublicRepoProofBadge",
"tags": [
"Public"
],
"summary": "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "owner",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": true,
"name": "repo",
"in": "path"
}
],
"responses": {
"200": {
"description": "SVG badge"
},
"404": {
"description": "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image"
},
"503": {
"description": "Same neutral SVG on an internal error"
}
}
}
}
},
"servers": [
Expand Down
14 changes: 14 additions & 0 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1507,3 +1507,17 @@ settings:
# # To stop trusting a peer, remove its key -- that is the whole revocation story, by design.
# peerKeys:
# - 0000000000000000000000000000000000000000000000000000000000000000

# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page
# (`/proof/<owner>/<repo>`) and its README badge.
#
# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the
# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already
# curl would add friction without adding privacy. This block exists because a page is nonetheless a
# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers.
#
# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn
# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely
# to keep the page on once the operator enables it.
# publicProof:
# enabled: false # Bool. Default when the block is absent: true (opt-out).
45 changes: 45 additions & 0 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,21 @@ export type FocusManifestPublicStatsConfig = {
enabled: boolean;
};

/**
* Per-repo opt-OUT for the public proof page and its badge (#9569), declared under `publicProof:`.
*
* Shape matches `publicStats:`/`ops:`, but the PRECEDENCE is deliberately different: those are fleet-wide
* and read from the operator's self-repo manifest, whereas this is read from the TARGET repo's own manifest,
* because the thing being opted out of is that repo's own page. Not present ⇒ enabled, once the operator's
* LOOPOVER_PUBLIC_PROOF flag is on — opt-OUT, since every figure the page renders is already publicly
* fetchable through the ledger-verify / anchors / decision-record endpoints. A repo can turn its page off;
* it cannot turn one on that the operator has not enabled.
*/
export type FocusManifestPublicProofConfig = {
present: boolean;
enabled: boolean;
};

/**
* Config-as-code override for the internal, bearer-gated contributor-trust-profile / fairness-analytics
* surface (LOOPOVER_FAIRNESS_ANALYTICS, #fairness-analytics), declared under `fairnessAnalytics:`. Same
Expand Down Expand Up @@ -1249,6 +1264,7 @@ export type FocusManifest = {
maintainerRecap: FocusManifestMaintainerRecapConfig;
ops: FocusManifestOpsConfig;
publicStats: FocusManifestPublicStatsConfig;
publicProof: FocusManifestPublicProofConfig;
fairnessAnalytics: FocusManifestFairnessAnalyticsConfig;
draftFlow: FocusManifestDraftFlowConfig;
upstreamDriftIssues: FocusManifestUpstreamDriftIssuesConfig;
Expand Down Expand Up @@ -1413,6 +1429,13 @@ const EMPTY_PUBLIC_STATS_CONFIG: FocusManifestPublicStatsConfig = {
enabled: false,
};

/** #9569: absent means ENABLED at the resolver (opt-out), so `enabled:false` here is only the shape's
* default — `present:false` is what the resolver actually keys on. */
const EMPTY_PUBLIC_PROOF_CONFIG: FocusManifestPublicProofConfig = {
present: false,
enabled: false,
};

const EMPTY_FAIRNESS_ANALYTICS_CONFIG: FocusManifestFairnessAnalyticsConfig = {
present: false,
enabled: false,
Expand Down Expand Up @@ -1478,6 +1501,7 @@ const EMPTY_MANIFEST: FocusManifest = {
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
ops: { ...EMPTY_OPS_CONFIG },
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
Expand Down Expand Up @@ -1520,6 +1544,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
ops: { ...EMPTY_OPS_CONFIG },
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
Expand Down Expand Up @@ -2356,6 +2381,24 @@ export function publicStatsConfigToJson(config: FocusManifestPublicStatsConfig):
return { enabled: config.enabled };
}

/** Parse the optional `publicProof:` mapping (#9569). Mirrors {@link parsePublicStatsConfig} exactly. */
function parsePublicProofConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestPublicProofConfig {
if (value === undefined || value === null) return { ...EMPTY_PUBLIC_PROOF_CONFIG };
if (typeof value !== "object" || Array.isArray(value)) {
warnings.push('Manifest field "publicProof" must be a mapping; ignoring it.');
return { ...EMPTY_PUBLIC_PROOF_CONFIG };
}
const record = value as Record<string, JsonValue>;
const enabled = normalizeOptionalBoolean(record.enabled, "publicProof.enabled", warnings) ?? false;
return { present: true, enabled };
}

/** Serialize a publicProof config so a cached snapshot round-trips through {@link parsePublicProofConfig}. */
export function publicProofConfigToJson(config: FocusManifestPublicProofConfig): JsonValue {
if (!config.present) return null;
return { enabled: config.enabled };
}

/** Parse the optional `fairnessAnalytics:` mapping (#fairness-analytics). Mirrors {@link parsePublicStatsConfig}
* exactly -- the only field is `enabled`, no DB layer to overlay onto. */
function parseFairnessAnalyticsConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestFairnessAnalyticsConfig {
Expand Down Expand Up @@ -4247,6 +4290,7 @@ export const FOCUS_MANIFEST_TOP_LEVEL_FIELDS = [
"maintainerRecap",
"ops",
"publicStats",
"publicProof",
"draftFlow",
"upstreamDriftIssues",
"sweepWatchdog",
Expand Down Expand Up @@ -4326,6 +4370,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
maintainerRecap: parseMaintainerRecapConfig(record.maintainerRecap, warnings),
ops: parseOpsConfig(record.ops, warnings),
publicStats: parsePublicStatsConfig(record.publicStats, warnings),
publicProof: parsePublicProofConfig(record.publicProof, warnings),
fairnessAnalytics: parseFairnessAnalyticsConfig(record.fairnessAnalytics, warnings),
draftFlow: parseDraftFlowConfig(record.draftFlow, warnings),
upstreamDriftIssues: parseUpstreamDriftIssuesConfig(record.upstreamDriftIssues, warnings),
Expand Down
39 changes: 39 additions & 0 deletions src/api/proof-badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// README badge for the public proof page (#9569). Renders through the SAME flat-badge primitive and the
// SAME XML escaping as the repo-quality badge (`./badge.ts`), so an unauthenticated, embeddable surface
// cannot become an injection vector and the two badges cannot drift apart visually.
import { escapeXml } from "./badge";
import { buildProofBadgeColor, buildProofBadgeMessage, type ProofSummary } from "../review/proof-summary";

export const PROOF_BADGE_LABEL = "loopover proof";
const UNAVAILABLE_COLOR = "#9e9e9e";

/** `null` renders the neutral unavailable badge — used for both the flag-off 404 and the error 503, since
* from a README's point of view those are the same thing: no claim is being made right now. */
export function renderProofBadgeSvg(summary: ProofSummary | null): string {
const message = summary ? buildProofBadgeMessage(summary) : "unavailable";
const color = summary ? buildProofBadgeColor(summary) : UNAVAILABLE_COLOR;
return renderFlatBadge(PROOF_BADGE_LABEL, message, color);
}

function renderFlatBadge(label: string, message: string, color: string): string {
const labelText = escapeXml(label);
const messageText = escapeXml(message);
const labelWidth = textWidth(label);
const messageWidth = textWidth(message);
const totalWidth = labelWidth + messageWidth;
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${labelText}: ${messageText}">`,
`<title>${labelText}: ${messageText}</title>`,
`<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>`,
`<rect width="${labelWidth}" height="20" rx="3" fill="#24292f"/>`,
`<rect x="${labelWidth}" width="${messageWidth}" height="20" rx="3" fill="${escapeXml(color)}"/>`,
`<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">`,
`<text x="${labelWidth / 2}" y="14">${labelText}</text>`,
`<text x="${labelWidth + messageWidth / 2}" y="14">${messageText}</text>`,
`</g></svg>`,
].join("");
}

function textWidth(text: string): number {
return Math.max(40, Math.round(text.length * 6.5) + 10);
}
37 changes: 37 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ import { isRagEnabled } from "../review/rag-wire";
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, publicAnchorStatus, signLedgerAnchorPayload } from "../review/ledger-anchor";
import { resolveProofPage } from "../review/proof-summary";
import { renderProofBadgeSvg } from "./proof-badge";
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
Expand Down Expand Up @@ -1366,6 +1368,41 @@ export function createApp() {
});
});

// #9569: the public proof page's data, and its README badge. Read-only over the SAME public sources the
// standalone endpoints already serve -- no new verification mechanism and no new SQL surface.
//
// Both handlers are thin renderers over ONE resolver (resolveProofPage): the gate, the read and the
// failure outcome are decided there, so the page and the badge cannot disagree about whether a repo is
// published. That is not a stylistic preference -- the gate previously lived inline in both bodies and
// exactly one of them was wired to the per-repo opt-out.
const proofPageDeps = {
loadManifest: loadRepoFocusManifest,
verifyLedger: (env: Env) => verifyDecisionLedger(env),
loadAnchors: (env: Env) => loadPublicLedgerAnchors(env, { limit: 20 }),
};

app.get("/v1/public/repos/:owner/:repo/proof", async (c) => {
const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps);
if (result.status === "disabled") return c.json({ error: "not_found" }, 404);
c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
return c.json(result.summary);
});

// The badge deliberately 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 (which does not fit
// in a badge) is exactly the bare scalar the proof summary refuses to publish. A disabled repo renders
// the neutral badge rather than an error -- a broken image in a README is worse than an honest one.
app.get("/v1/public/repos/:owner/:repo/proof-badge.svg", async (c) => {
c.header("Content-Type", "image/svg+xml; charset=utf-8");
const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps);
if (result.status === "disabled") {
c.header("Cache-Control", "public, max-age=300");
return c.body(renderProofBadgeSvg(null), 404);
}
c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
return c.body(renderProofBadgeSvg(result.summary));
});

// #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor
// commitment submitter to fetch and commit on-chain (sha256 of `signingInput` is the exact 32 bytes
// `Data::Sha256` holds). Unauthenticated like every /v1/public/* sibling: it is the same payload the
Expand Down
5 changes: 5 additions & 0 deletions src/auth/route-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export function requiresApiToken(path: string): boolean {
if (path === "/v1/public/decision-ledger/anchor-key") return false;
// #9271: the public anchor-attempt listing, added in the SAME PR as its route.
if (path === "/v1/public/decision-ledger/anchors") return false;
// #9569: the public proof page's data and its README badge. Unauthenticated by design -- every figure
// they render is already served unauthenticated by the ledger-verify / anchors / decision-record routes
// above; this is a composition, not a new disclosure. Added in the SAME PR as the routes, per #9120.
if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof$/.test(path)) return false;
if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof-badge\.svg$/.test(path)) return false;
// #9277: the current tip's signed checkpoint, for the operator's off-Worker Bittensor submitter (and
// anyone else — it is the same payload the Rekor/git backends already publish externally). Added in the
// SAME PR as its route, per the #9120 lesson.
Expand Down
4 changes: 4 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,10 @@ declare global {
* worker is byte-identical to today. Exposes review-disposition counts + a reversal-grounded accuracy
* percentage + an estimated-time-saved figure ONLY — never PR content, authors, scores, or reward internals.
* See review/public-stats.ts. */
/** #9569: fleet-wide switch for the public proof page (`/proof/:owner/:repo`) and its badge. Default
* OFF like every sibling public surface. A repo can opt OUT via its own manifest, but cannot opt IN
* when this is off -- see isProofPageEnabledForRepo's recorded decision in review/proof-summary.ts. */
LOOPOVER_PUBLIC_PROOF?: string;
LOOPOVER_PUBLIC_STATS?: string;
/** Proof of Power (#1059): comma-separated allowlist of repo full-names ("owner/repo") whose OWN historical
* review ledger (audit_events "published a review surface" + pull_requests terminal state) counts toward
Expand Down
26 changes: 26 additions & 0 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,32 @@ export function buildOpenApiSpec() {
200: { description: "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore, status } — a failed attempt is returned identically to a successful one, never filtered out or reshaped. The top-level `status` (anchored | empty_ledger | unconfigured | pending) says why the list looks as it does, so an empty list cannot be mistaken for a healthy one; it is omitted when a backend/before filter is applied, where empty just means none matched" },
},
});
registry.registerPath({
method: "get",
path: "/v1/public/repos/{owner}/{repo}/proof",
operationId: "getPublicRepoProof",
tags: ["Public"],
summary: "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
200: { description: "ProofSummary. Any accuracy figure carries its coverage AND a Wilson confidence interval; below the sample floor it is an explicit `insufficient_data` state, never a bare percentage. Carries the verification-boundary statement in the payload" },
404: { description: "The proof page is disabled fleet-wide, or this repo has opted out" },
503: { description: "Composition failed — no partial or fabricated summary is served" },
},
});
registry.registerPath({
method: "get",
path: "/v1/public/repos/{owner}/{repo}/proof-badge.svg",
operationId: "getPublicRepoProofBadge",
tags: ["Public"],
summary: "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
200: { description: "SVG badge" },
404: { description: "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image" },
503: { description: "Same neutral SVG on an internal error" },
},
});
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/anchor-payload",
Expand Down
Loading
Loading