Skip to content

Commit 26f90ca

Browse files
committed
fix(proof): actually wire the per-repo opt-out the routes only claimed to honor (#9569)
Review caught the real defect: both handlers called isProofPageEnabledForRepo(c.env) with no second argument, so the ProofPageRepoOverride documented at length in proof-summary.ts and in the PR body was never loaded or passed. Every repo was effectively opt-out-less once the fleet flag was on -- a gate that is described, typed, and unit-tested as a pure function, but never reachable from the surface it governs. That is the registered-but-unreachable class, and the long comment made it worse rather than better by making it look done. - Adds a real `publicProof:` focus-manifest block (engine parser + toJson + loader snapshot), mirroring `publicStats:`/`ops:`. Precedence is deliberately the opposite of those two: read from the TARGET repo's manifest rather than the operator's self-repo, because the thing being opted out of is that repo's own page. - loadProofPageRepoOverride resolves it, degrading a failed manifest load to "no override" -- a broken manifest never takes a page DOWN, which is the failure direction worth accepting here and is now stated in the doc comment rather than left implicit. - Both routes load the override BEFORE anything else, so a repo that turned its page off does not have its decision records queried to build a summary that will be discarded. - Documents the block in .loopover.yml.example, including the precedence and the opt-out default. Tests that would have caught it: a repo opting out in its manifest now gets 404 from BOTH routes with the fleet flag on, while a different repo in the same fleet still serves 200 (the opt-out is per repo, not a kill switch); explicit opt-in and no-block-at-all both serve; and the resolver is covered across absent/explicit/failing loads.
1 parent 63251f5 commit 26f90ca

8 files changed

Lines changed: 134 additions & 6 deletions

File tree

.loopover.yml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,3 +1493,17 @@ settings:
14931493
# # To stop trusting a peer, remove its key -- that is the whole revocation story, by design.
14941494
# peerKeys:
14951495
# - 0000000000000000000000000000000000000000000000000000000000000000
1496+
1497+
# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page
1498+
# (`/proof/<owner>/<repo>`) and its README badge.
1499+
#
1500+
# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the
1501+
# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already
1502+
# curl would add friction without adding privacy. This block exists because a page is nonetheless a
1503+
# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers.
1504+
#
1505+
# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn
1506+
# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely
1507+
# to keep the page on once the operator enables it.
1508+
# publicProof:
1509+
# enabled: false # Bool. Default when the block is absent: true (opt-out).

packages/loopover-engine/src/focus-manifest.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,21 @@ export type FocusManifestPublicStatsConfig = {
440440
enabled: boolean;
441441
};
442442

443+
/**
444+
* Per-repo opt-OUT for the public proof page and its badge (#9569), declared under `publicProof:`.
445+
*
446+
* Shape matches `publicStats:`/`ops:`, but the PRECEDENCE is deliberately different: those are fleet-wide
447+
* and read from the operator's self-repo manifest, whereas this is read from the TARGET repo's own manifest,
448+
* because the thing being opted out of is that repo's own page. Not present ⇒ enabled, once the operator's
449+
* LOOPOVER_PUBLIC_PROOF flag is on — opt-OUT, since every figure the page renders is already publicly
450+
* fetchable through the ledger-verify / anchors / decision-record endpoints. A repo can turn its page off;
451+
* it cannot turn one on that the operator has not enabled.
452+
*/
453+
export type FocusManifestPublicProofConfig = {
454+
present: boolean;
455+
enabled: boolean;
456+
};
457+
443458
/**
444459
* Config-as-code override for the internal, bearer-gated contributor-trust-profile / fairness-analytics
445460
* surface (LOOPOVER_FAIRNESS_ANALYTICS, #fairness-analytics), declared under `fairnessAnalytics:`. Same
@@ -1249,6 +1264,7 @@ export type FocusManifest = {
12491264
maintainerRecap: FocusManifestMaintainerRecapConfig;
12501265
ops: FocusManifestOpsConfig;
12511266
publicStats: FocusManifestPublicStatsConfig;
1267+
publicProof: FocusManifestPublicProofConfig;
12521268
fairnessAnalytics: FocusManifestFairnessAnalyticsConfig;
12531269
draftFlow: FocusManifestDraftFlowConfig;
12541270
upstreamDriftIssues: FocusManifestUpstreamDriftIssuesConfig;
@@ -1413,6 +1429,13 @@ const EMPTY_PUBLIC_STATS_CONFIG: FocusManifestPublicStatsConfig = {
14131429
enabled: false,
14141430
};
14151431

1432+
/** #9569: absent means ENABLED at the resolver (opt-out), so `enabled:false` here is only the shape's
1433+
* default — `present:false` is what the resolver actually keys on. */
1434+
const EMPTY_PUBLIC_PROOF_CONFIG: FocusManifestPublicProofConfig = {
1435+
present: false,
1436+
enabled: false,
1437+
};
1438+
14161439
const EMPTY_FAIRNESS_ANALYTICS_CONFIG: FocusManifestFairnessAnalyticsConfig = {
14171440
present: false,
14181441
enabled: false,
@@ -1478,6 +1501,7 @@ const EMPTY_MANIFEST: FocusManifest = {
14781501
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
14791502
ops: { ...EMPTY_OPS_CONFIG },
14801503
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
1504+
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
14811505
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
14821506
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
14831507
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
@@ -1520,6 +1544,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
15201544
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
15211545
ops: { ...EMPTY_OPS_CONFIG },
15221546
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
1547+
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
15231548
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
15241549
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
15251550
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
@@ -2356,6 +2381,24 @@ export function publicStatsConfigToJson(config: FocusManifestPublicStatsConfig):
23562381
return { enabled: config.enabled };
23572382
}
23582383

2384+
/** Parse the optional `publicProof:` mapping (#9569). Mirrors {@link parsePublicStatsConfig} exactly. */
2385+
function parsePublicProofConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestPublicProofConfig {
2386+
if (value === undefined || value === null) return { ...EMPTY_PUBLIC_PROOF_CONFIG };
2387+
if (typeof value !== "object" || Array.isArray(value)) {
2388+
warnings.push('Manifest field "publicProof" must be a mapping; ignoring it.');
2389+
return { ...EMPTY_PUBLIC_PROOF_CONFIG };
2390+
}
2391+
const record = value as Record<string, JsonValue>;
2392+
const enabled = normalizeOptionalBoolean(record.enabled, "publicProof.enabled", warnings) ?? false;
2393+
return { present: true, enabled };
2394+
}
2395+
2396+
/** Serialize a publicProof config so a cached snapshot round-trips through {@link parsePublicProofConfig}. */
2397+
export function publicProofConfigToJson(config: FocusManifestPublicProofConfig): JsonValue {
2398+
if (!config.present) return null;
2399+
return { enabled: config.enabled };
2400+
}
2401+
23592402
/** Parse the optional `fairnessAnalytics:` mapping (#fairness-analytics). Mirrors {@link parsePublicStatsConfig}
23602403
* exactly -- the only field is `enabled`, no DB layer to overlay onto. */
23612404
function parseFairnessAnalyticsConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestFairnessAnalyticsConfig {
@@ -4326,6 +4369,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
43264369
maintainerRecap: parseMaintainerRecapConfig(record.maintainerRecap, warnings),
43274370
ops: parseOpsConfig(record.ops, warnings),
43284371
publicStats: parsePublicStatsConfig(record.publicStats, warnings),
4372+
publicProof: parsePublicProofConfig(record.publicProof, warnings),
43294373
fairnessAnalytics: parseFairnessAnalyticsConfig(record.fairnessAnalytics, warnings),
43304374
draftFlow: parseDraftFlowConfig(record.draftFlow, warnings),
43314375
upstreamDriftIssues: parseUpstreamDriftIssuesConfig(record.upstreamDriftIssues, warnings),

src/api/routes.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ import { isRagEnabled } from "../review/rag-wire";
306306
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
307307
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
308308
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, signLedgerAnchorPayload } from "../review/ledger-anchor";
309-
import { isProofPageEnabledForRepo, loadProofSummary } from "../review/proof-summary";
309+
import { isProofPageEnabledForRepo, loadProofPageRepoOverride, loadProofSummary } from "../review/proof-summary";
310310
import { renderProofBadgeSvg } from "./proof-badge";
311311
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
312312
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
@@ -1357,8 +1357,11 @@ export function createApp() {
13571357
// must allow (see isProofPageEnabledForRepo's recorded opt-out decision): the operator's fleet-wide flag,
13581358
// default OFF like every sibling public surface, and the repo's own opt-out.
13591359
app.get("/v1/public/repos/:owner/:repo/proof", async (c) => {
1360-
if (!isProofPageEnabledForRepo(c.env)) return c.json({ error: "not_found" }, 404);
13611360
const repoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
1361+
// The repo's OWN opt-out, loaded before anything else is read: a repo that turned its page off must not
1362+
// have its decision records queried to build a summary that will be discarded.
1363+
const override = await loadProofPageRepoOverride(c.env, repoFullName, loadRepoFocusManifest);
1364+
if (!isProofPageEnabledForRepo(c.env, override)) return c.json({ error: "not_found" }, 404);
13621365
try {
13631366
const summary = await loadProofSummary(c.env, repoFullName, {
13641367
verifyLedger: (env) => verifyDecisionLedger(env),
@@ -1376,12 +1379,14 @@ export function createApp() {
13761379
// in a badge) is exactly the bare scalar the proof summary refuses to publish.
13771380
app.get("/v1/public/repos/:owner/:repo/proof-badge.svg", async (c) => {
13781381
c.header("Content-Type", "image/svg+xml; charset=utf-8");
1379-
if (!isProofPageEnabledForRepo(c.env)) {
1382+
const badgeRepoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
1383+
const badgeOverride = await loadProofPageRepoOverride(c.env, badgeRepoFullName, loadRepoFocusManifest);
1384+
if (!isProofPageEnabledForRepo(c.env, badgeOverride)) {
13801385
c.header("Cache-Control", "public, max-age=300");
13811386
return c.body(renderProofBadgeSvg(null), 404);
13821387
}
13831388
try {
1384-
const summary = await loadProofSummary(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, {
1389+
const summary = await loadProofSummary(c.env, badgeRepoFullName, {
13851390
verifyLedger: (env) => verifyDecisionLedger(env),
13861391
loadAnchors: (env) => loadPublicLedgerAnchors(env, { limit: 20 }),
13871392
});

src/review/proof-summary.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,29 @@ export function buildProofBadgeColor(summary: ProofSummary): string {
217217

218218
export type ProofPageRepoOverride = { present: boolean; enabled: boolean };
219219

220+
/**
221+
* Load ONE repo's `publicProof:` opt-out from its own focus manifest.
222+
*
223+
* Read from the TARGET repo's manifest, not the operator's self-repo, because the thing being opted out of
224+
* is that repo's own page -- the opposite precedence from `publicStats:`/`ops:`, which are fleet-wide.
225+
*
226+
* A manifest load failure degrades to `{ present: false }`, i.e. exactly as if no override existed, so a
227+
* network blip or malformed YAML can never accidentally EXPOSE a page the maintainer turned off... which is
228+
* the wrong direction, and is why the caller must treat a failed load as the operator default rather than
229+
* this function pretending to know. Documented here because the failure direction is the interesting part:
230+
* we accept "a broken manifest leaves the page on" in exchange for "a broken manifest never takes a page
231+
* down", matching how every other resolveX accessor in this codebase degrades.
232+
*/
233+
export async function loadProofPageRepoOverride(
234+
env: Env,
235+
repoFullName: string,
236+
loadManifest: (env: Env, repoFullName: string) => Promise<{ publicProof: { present: boolean; enabled: boolean } } | null>,
237+
): Promise<ProofPageRepoOverride> {
238+
const manifest = await loadManifest(env, repoFullName).catch(() => null);
239+
if (!manifest?.publicProof?.present) return { present: false, enabled: false };
240+
return { present: true, enabled: manifest.publicProof.enabled };
241+
}
242+
220243
/** Fleet-wide operator flag -- truthy-string, default OFF, matching isPublicStatsEnabled's convention. */
221244
export function isPublicProofPageEnabled(env: { LOOPOVER_PUBLIC_PROOF?: string | undefined }): boolean {
222245
return /^(1|true|yes|on)$/i.test(env.LOOPOVER_PUBLIC_PROOF ?? "");

src/signals/focus-manifest-loader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { listSignalSnapshots, persistSignalSnapshot } from "../db/repositories";
22
import { mapWithConcurrency } from "../queue/map-with-concurrency";
33
import type { JsonValue } from "../types";
44
import { nowIso } from "../utils/json";
5-
import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, loopEscalationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest";
5+
import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, publicProofConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, loopEscalationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest";
66
import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML, resolveLoopOverSelfRepoFullName } from "../config/loopover-repo-focus-manifest";
77
import type { LocalManifestLoadResult } from "../selfhost/private-config";
88

@@ -337,6 +337,7 @@ function manifestToJson(manifest: FocusManifest): Record<string, JsonValue> {
337337
maintainerRecap: maintainerRecapConfigToJson(manifest.maintainerRecap),
338338
ops: opsConfigToJson(manifest.ops),
339339
publicStats: publicStatsConfigToJson(manifest.publicStats),
340+
publicProof: publicProofConfigToJson(manifest.publicProof),
340341
fairnessAnalytics: fairnessAnalyticsConfigToJson(manifest.fairnessAnalytics),
341342
draftFlow: draftFlowConfigToJson(manifest.draftFlow),
342343
upstreamDriftIssues: upstreamDriftIssuesConfigToJson(manifest.upstreamDriftIssues),

src/signals/focus-manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export {
3737
maintainerRecapConfigToJson,
3838
opsConfigToJson,
3939
publicStatsConfigToJson,
40+
publicProofConfigToJson,
4041
fairnessAnalyticsConfigToJson,
4142
draftFlowConfigToJson,
4243
upstreamDriftIssuesConfigToJson,

test/unit/focus-manifest.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ describe("compileFocusManifestPolicy", () => {
971971
maintainerRecap: { present: false, enabled: false, cadence: "weekly", channel: "discord" },
972972
ops: { present: false, enabled: false },
973973
publicStats: { present: false, enabled: false },
974+
publicProof: { present: false, enabled: false },
974975
fairnessAnalytics: { present: false, enabled: false },
975976
draftFlow: { present: false, enabled: false },
976977
upstreamDriftIssues: { present: false, enabled: false },

test/unit/proof-summary.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { createApp } from "../../src/api/routes";
1818
import { appendDecisionLedger, persistDecisionRecord } from "../../src/review/decision-record";
1919
import { loadPublicLedgerAnchors, recordLedgerAnchorAttempt } from "../../src/review/ledger-anchor-persistence";
2020
import { createTestEnv } from "../helpers/d1";
21+
import { loadProofPageRepoOverride } from "../../src/review/proof-summary";
22+
import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader";
2123
import type { PublicLedgerAnchor } from "../../src/review/ledger-anchor-persistence";
2224

2325
// #9569: the public, shareable twin of the in-app trust panel. The properties that matter here are the ones
@@ -326,4 +328,41 @@ describe("loadProofSummary + routes (#9569)", () => {
326328
});
327329
expect(summary.anchor).toMatchObject({ state: "anchored", backend: "rekor", seq: 3 });
328330
});
329-
});
331+
332+
it("REGRESSION: a repo that opts OUT in its manifest gets 404 from BOTH routes, even with the fleet flag on", async () => {
333+
// The defect this test exists for: both handlers called isProofPageEnabledForRepo(c.env) with no
334+
// override, so the documented per-repo opt-out was never loaded and every repo was effectively
335+
// opt-out-less once the operator flag was on.
336+
const app = createApp();
337+
const env = await seeded();
338+
await upsertRepoFocusManifest(env, "o/r", { publicProof: { enabled: false } } as never);
339+
340+
expect((await app.request("/v1/public/repos/o/r/proof", {}, env)).status).toBe(404);
341+
const badge = await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, env);
342+
expect(badge.status).toBe(404);
343+
expect(await badge.text()).toContain("unavailable");
344+
345+
// A DIFFERENT repo in the same fleet is unaffected — the opt-out is per repo, not a kill switch.
346+
expect((await app.request("/v1/public/repos/other/repo/proof", {}, env)).status).toBe(200);
347+
});
348+
349+
it("an explicit manifest opt-IN serves, and so does a repo with no manifest block at all (opt-out default)", async () => {
350+
const app = createApp();
351+
const env = await seeded();
352+
await upsertRepoFocusManifest(env, "o/r", { publicProof: { enabled: true } } as never);
353+
expect((await app.request("/v1/public/repos/o/r/proof", {}, env)).status).toBe(200);
354+
// No block at all: the page is on, because the data is already public and this is opt-OUT.
355+
const bare = await seeded();
356+
expect((await app.request("/v1/public/repos/o/r/proof", {}, bare)).status).toBe(200);
357+
});
358+
359+
it("loadProofPageRepoOverride: absent block, explicit values, and a failing load all resolve honestly", async () => {
360+
const env = createTestEnv();
361+
expect(await loadProofPageRepoOverride(env, "o/r", async () => null)).toEqual({ present: false, enabled: false });
362+
expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: false, enabled: false } }))).toEqual({ present: false, enabled: false });
363+
expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: true, enabled: false } }))).toEqual({ present: true, enabled: false });
364+
expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: true, enabled: true } }))).toEqual({ present: true, enabled: true });
365+
// A failing manifest load degrades to "no override" -- a broken manifest never takes a page DOWN.
366+
expect(await loadProofPageRepoOverride(env, "o/r", async () => { throw new Error("network down"); })).toEqual({ present: false, enabled: false });
367+
});
368+
});

0 commit comments

Comments
 (0)