diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 138b562334..8fbfc6d0ca 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -384,6 +384,7 @@ import { } from "../signals/focus-manifest"; import { decideReviewEligibility } from "../review/review-eligibility"; import { + loadPublicRepoFocusManifest, loadRepoFocusManifest, loadRepoFocusManifests, loadRepoReviewContext, @@ -9542,9 +9543,12 @@ async function maybePublishPrPublicSurface( } if (decision.willComment) { - // Maintainer review-content overrides from `.gittensory.yml` (footer text, row toggles, intro note). - // Cached, so this is a DB read after the settings resolution already loaded the manifest. - const repoFocusManifestForComment = await loadRepoFocusManifest(env, repoFullName); + // Maintainer review-content overrides may come from private self-host config, but validation warnings + // rendered in the public PR comment must come only from the repo-published manifest. + const [repoFocusManifestForComment, publicRepoFocusManifestForComment] = await Promise.all([ + loadRepoFocusManifest(env, repoFullName), + loadPublicRepoFocusManifest(env, repoFullName).catch(() => null), + ]); const reviewConfig = repoFocusManifestForComment.review; // Duplicate-winner adjudication (#dup-winner): thread the flag into the public panel builders so the // winner's hard-duplicate block is suppressed (they recompute the winner from their own open-only sibling @@ -9856,10 +9860,10 @@ async function maybePublishPrPublicSurface( : {}), maxFindingsCaps: reviewConfig.maxFindings, commentVerbosity: reviewConfig.commentVerbosity, - // review-manifest validation (#2056): reuse the same manifest already loaded above for reviewConfig — - // unconditional (no manifest opt-in needed, a broken config should always fail clearly); no warnings - // ⇒ the bridge omits the section (byte-identical). - manifestWarnings: repoFocusManifestForComment.warnings, + // review-manifest validation (#2056): public PR comments may disclose only repo-published manifest + // warnings. Self-host private config can carry operator-only policy and raw invalid values, so never + // render warnings from the full/private manifest here. + manifestWarnings: publicRepoFocusManifestForComment?.warnings ?? [], }); } else { deterministicBody = buildPublicPrIntelligenceComment(commentArgs); diff --git a/test/unit/manifest-validation-collapsible.test.ts b/test/unit/manifest-validation-collapsible.test.ts index 139ebf596c..905b113714 100644 --- a/test/unit/manifest-validation-collapsible.test.ts +++ b/test/unit/manifest-validation-collapsible.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { buildManifestValidationCollapsible, buildUnifiedCommentBody } from "../../src/review/unified-comment-bridge"; +import { loadPublicRepoFocusManifest, setLocalManifestReader } from "../../src/signals/focus-manifest-loader"; +import { createTestEnv } from "../helpers/d1"; import type { GateCheckEvaluation } from "../../src/rules/advisory"; import type { PublicPrPanelSignalRow } from "../../src/signals/engine"; @@ -43,6 +45,35 @@ describe("buildManifestValidationCollapsible (#2056)", () => { }); describe("buildUnifiedCommentBody: manifest validation wiring (#2056)", () => { + afterEach(() => setLocalManifestReader(null)); + + it("uses only repo-published warnings for public comments, not private self-host manifest warnings", async () => { + const env = createTestEnv(); + const secret = "OPERATOR_ONLY_SECRET_9f3c"; + setLocalManifestReader(async () => `gate: + linkedIssue: "wallet hotkey reward scoreability ${secret}" +`); + + const publicManifest = await loadPublicRepoFocusManifest(env, "owner/repo", { + fetcher: async () => `review: + profile: loud +`, + }); + const body = buildUnifiedCommentBody({ + gate: gate(), + panelRows, + readinessTotal: 88, + changedFiles: 1, + footerMarkdown: footer, + manifestWarnings: publicManifest.warnings, + }); + + expect(body).toContain("Manifest validation"); + expect(body).toContain('Manifest "review.profile"'); + expect(body).not.toContain(secret); + expect(body).not.toMatch(/wallet|hotkey|reward|scoreability/i); + }); + it("appends a Manifest validation collapsible when manifestWarnings is non-empty", () => { const body = buildUnifiedCommentBody({ gate: gate(),