From 783c612afa2a79265ee05d84c41ca0df1b1b5629 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:06:32 -0700 Subject: [PATCH 1/2] fix(selfhost): protect private policy surfaces --- src/mcp/server.ts | 6 ++-- src/queue/processors.ts | 33 ++++++++++++++++----- test/unit/mcp-predict-gate.test.ts | 47 ++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 220332d5..8768332b 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -114,7 +114,7 @@ import { applyStepResult, buildPlanDag, nextReadySteps, planProgress, validatePl import { isGlobalAgentPause, resolveAgentActionMode, resolveAgentPermissionReadiness } from "../settings/agent-execution"; import { AGENT_ACTION_CLASSES, isActingAutonomyLevel, resolveAutonomy } from "../settings/autonomy"; import { MAX_FOCUS_MANIFEST_BYTES } from "../signals/focus-manifest"; -import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; +import { loadPublicRepoFocusManifest, loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import { buildPredictedGateVerdict } from "../rules/predicted-gate"; import { buildIssueSlopAssessment, buildSlopAssessment } from "../signals/slop"; import { buildRepoDataQuality } from "../signals/data-quality"; @@ -2067,7 +2067,7 @@ export class GittensoryMcp { listPullRequests(this.env, repoFullName), listBountiesByRepo(this.env, repoFullName), loadOrComputeIssueQualityResponse(this.env, repoFullName), - loadRepoFocusManifest(this.env, repoFullName), + loadPublicRepoFocusManifest(this.env, repoFullName), ]); // Resolve the caller's own confirmed-Gittensor status the same way the maintainer pipeline does (official // Gittensor API → confirmed). It is surfaced in the verdict for transparency but no longer changes the @@ -2687,7 +2687,7 @@ export class GittensoryMcp { listBountiesByRepo(this.env, input.repoFullName), getOrCreateScoringModelSnapshot(this.env), loadOrComputeIssueQualityResponse(this.env, input.repoFullName), - loadRepoFocusManifest(this.env, input.repoFullName), + loadPublicRepoFocusManifest(this.env, input.repoFullName), ]); const fit = buildContributorFit(context.profile, context.repositories, [], [], context.syncStates, context.repoStats); const scoringProfile = buildContributorScoringProfile({ login: input.login, fit, scoringSnapshot: snapshot }); diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 7e580971..5c42c160 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -174,7 +174,7 @@ import { renderReviewingPlaceholder, shouldPostReviewingPlaceholder, type CheckF import { buildIssueSlopAssessment, buildSlopAssessment, type SlopBand } from "../signals/slop"; import { runGittensoryAiSlopAdvisory } from "../services/ai-slop"; import { decidePublicSurface } from "../signals/settings-preview"; -import { buildFocusManifestGuidance, excludeReviewPaths, resolveReviewPathInstructions, resolveReviewPreMergeChecks, resolveReviewPromptOverrides, type ReviewPathInstruction, type ReviewProfile } from "../signals/focus-manifest"; +import { buildFocusManifestGuidance, excludeReviewPaths, resolveReviewPathInstructions, resolveReviewPreMergeChecks, resolveReviewPromptOverrides, type FocusManifestFinding, type ReviewPathInstruction, type ReviewProfile } from "../signals/focus-manifest"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import { resolveRepositorySettings } from "../settings/repository-settings"; import type { LocalBranchAnalysisInput } from "../signals/local-branch"; @@ -225,6 +225,29 @@ export async function runRetentionPrune(env: Env, requestedBy: string, dryRun: b }); } +const PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES: Partial>> = { + manifest_blocked_path: { + detail: "Changed paths match maintainer-blocked areas.", + action: "Move this work out of the maintainer-blocked area or confirm with the maintainer before opening a PR.", + }, + manifest_missing_tests: { + detail: "Maintainer test expectations are not satisfied by this PR.", + action: "Add or update tests, or attach passing validation output that satisfies the maintainer's test expectations.", + }, +}; + +function publicSafeManifestPolicyFinding(finding: FocusManifestFinding): AdvisoryFinding { + const fallback = finding.action === undefined ? {} : { action: finding.action }; + return { + code: finding.code, + severity: finding.severity, + title: finding.title, + detail: finding.detail, + ...fallback, + ...PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES[finding.code], + }; +} + export async function processJob(env: Env, message: JobMessage): Promise { switch (message.type) { case "refresh-registry": @@ -2616,13 +2639,7 @@ async function maybePublishPrPublicSurface( const policyCodes = new Set(["manifest_blocked_path", "manifest_linked_issue_required", "manifest_missing_tests"]); for (const finding of guidance.findings) { if (!policyCodes.has(finding.code)) continue; - advisory.findings.push({ - code: finding.code, - severity: finding.severity, - title: finding.title, - detail: finding.detail, - ...(finding.action !== undefined ? { action: finding.action } : {}), - }); + advisory.findings.push(publicSafeManifestPolicyFinding(finding)); } } // Pre-merge checks (#review-pre-merge-checks, opt-in via .gittensory.yml review.pre_merge_checks). DETERMINISTIC diff --git a/test/unit/mcp-predict-gate.test.ts b/test/unit/mcp-predict-gate.test.ts index 085f70ee..ac2285e8 100644 --- a/test/unit/mcp-predict-gate.test.ts +++ b/test/unit/mcp-predict-gate.test.ts @@ -3,7 +3,7 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { afterEach, describe, expect, it, vi } from "vitest"; import { GittensoryMcp } from "../../src/mcp/server"; import { createSessionForGitHubUser, type AuthIdentity } from "../../src/auth/security"; -import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import { setLocalManifestReader, upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { upsertRepositoryFromGitHub } from "../../src/db/repositories"; import { createTestEnv } from "../helpers/d1"; @@ -17,12 +17,13 @@ async function connect(env: Env, identity?: AuthIdentity) { } describe("MCP gittensory_predict_gate", () => { + afterEach(() => setLocalManifestReader(null)); it("predicts the gate from public config on an unregistered repo under oss-anti-slop", async () => { const env = createTestEnv(); // A non-Gittensor repo: app-installed (so gittensory has "seen" it) but NOT Gittensor-registered, with // public config only (gate.pack oss-anti-slop, linked-issue blocks any author). await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); - await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", linkedIssue: "block" } }); + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", linkedIssue: "block" } }, "repo_file"); const client = await connect(env); const result = await client.callTool({ @@ -66,6 +67,40 @@ describe("MCP gittensory_predict_gate", () => { expect(data.note.toLowerCase()).toContain("slop"); }); + it("ignores container-private manifests and predicts from the public repo file", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", linkedIssue: "off", readiness: { mode: "off" } } }, "repo_file"); + setLocalManifestReader(async (repo) => + repo === "acme/widgets" + ? `gate: + pack: oss-anti-slop + linkedIssue: block + readiness: + mode: block + minScore: 99 +blockedPaths: + - secret/private/** +testExpectations: + - run private fuzz suite +` + : null, + ); + const client = await connect(env); + + const result = await client.callTool({ + name: "gittensory_predict_gate", + arguments: { login: "miner1", owner: "acme", repo: "widgets", title: "Public config only", linkedIssues: [] }, + }); + + expect(result.isError).toBeFalsy(); + const data = result.structuredContent as { pack: string; conclusion: string; blockers: Array<{ detail?: string }> }; + expect(data.pack).toBe("oss-anti-slop"); + expect(data.conclusion).toBe("success"); + expect(JSON.stringify(data)).not.toContain("secret/private/**"); + expect(JSON.stringify(data)).not.toContain("private fuzz suite"); + }); + // Parity (#gate-nonconfirmed): every author is gated identically now — a synthetic PR that trips a blocker // predicts `failure` regardless of confirmed status, matching the real maintainer gate. The prediction still // resolves + surfaces the caller's confirmed status (transparency / on-chain scoring context) but it no @@ -88,7 +123,7 @@ describe("MCP gittensory_predict_gate", () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); // gittensor pack, linked-issue blocks; the contributor supplies no linked issue → blocker fires. - await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } }); + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } }, "repo_file"); stubGittensorMiners([]); // miner1 is NOT a confirmed Gittensor contributor — gated the same regardless const client = await connect(env); @@ -108,7 +143,7 @@ describe("MCP gittensory_predict_gate", () => { it("predicts FAILURE for a confirmed contributor when the same blocker fires", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); - await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } }); + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } }, "repo_file"); stubGittensorMiners([{ login: "miner1", id: 4242 }]); // miner1 IS confirmed const client = await connect(env); @@ -127,7 +162,7 @@ describe("MCP gittensory_predict_gate", () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); // No explicit pack → defaults to the gittensor pack, which still resolves confirmed status via the API. - await upsertRepoFocusManifest(env, "acme/widgets", { gate: { linkedIssue: "block" } }); + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { linkedIssue: "block" } }, "repo_file"); // The confirmation lookup is the only network call on the prediction path (the URL is a fixed constant // base; the login is never interpolated into it — it is filtered client-side — so there is no SSRF // surface). When that call fails/times out, fetchGittensorContributorSnapshot resolves to null, so the @@ -154,7 +189,7 @@ describe("MCP gittensory_predict_gate", () => { it("is repo-scoped: a session cannot predict against an inaccessible repo", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "private-roadmap", full_name: "victimco/private-roadmap", private: true, owner: { login: "victimco" } }); - await upsertRepoFocusManifest(env, "victimco/private-roadmap", { gate: { pack: "oss-anti-slop", linkedIssue: "block" } }); + await upsertRepoFocusManifest(env, "victimco/private-roadmap", { gate: { pack: "oss-anti-slop", linkedIssue: "block" } }, "repo_file"); const { session } = await createSessionForGitHubUser(env, { login: "miner1", id: 1 }); const client = await connect(env, { kind: "session", actor: "miner1", session }); From c2ec70b095d55b0baf7429e61fada729d7e5acff Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:01:31 -0700 Subject: [PATCH 2/2] test(selfhost): assert manifest-policy redaction + rebase onto main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revive #1405: rebased onto current main (resolved the predict_gate test against the changedPaths work in #1413 — both tests kept), fixed the #1413 test to store a PUBLIC repo_file manifest now that predict_gate reads public-only config, inlined publicSafeManifestPolicyFinding with a v8-ignore on the unreachable no-action arm, exported it, and added a focused unit test asserting the private blocked-path / test-expectation detail is redacted out of the public advisory. --- src/queue/processors.ts | 8 +-- test/unit/mcp-predict-gate.test.ts | 3 +- .../unit/public-safe-manifest-finding.test.ts | 51 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 test/unit/public-safe-manifest-finding.test.ts diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 5c42c160..4e3aee34 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -236,14 +236,16 @@ const PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES: Partial { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" }); // Public config: oss-anti-slop (no account needed), manifest path policy in block mode, dist/** blocked. - await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", manifestPolicy: "block" }, blockedPaths: ["dist/**"] }); + // Stored as a PUBLIC repo_file manifest — predict_gate reads only public config (#selfhost-app-id / #1405). + await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", manifestPolicy: "block" }, blockedPaths: ["dist/**"] }, "repo_file"); const client = await connect(env); const result = await client.callTool({ diff --git a/test/unit/public-safe-manifest-finding.test.ts b/test/unit/public-safe-manifest-finding.test.ts new file mode 100644 index 00000000..283a0446 --- /dev/null +++ b/test/unit/public-safe-manifest-finding.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { publicSafeManifestPolicyFinding } from "../../src/queue/processors"; +import type { FocusManifestFinding } from "../../src/signals/focus-manifest"; + +// #1405 / #selfhost-app-id: the focus-manifest policy findings surfaced on the PUBLIC advisory must not echo the +// maintainer's private blocked-path globs or test expectations (which can come from a container-mounted config). +describe("publicSafeManifestPolicyFinding", () => { + it("redacts the private blocked-path detail to a static phrase, preserving code/severity/title", () => { + const finding: FocusManifestFinding = { + code: "manifest_blocked_path", + severity: "critical", + title: "Change touches a maintainer-blocked area", + detail: "Changed paths match maintainer-blocked patterns: secret/private/**, internal/keys/**.", + action: "Move this work elsewhere — secret/private/** is off-limits.", + }; + const safe = publicSafeManifestPolicyFinding(finding); + expect(safe.code).toBe("manifest_blocked_path"); + expect(safe.severity).toBe("critical"); + expect(safe.title).toBe("Change touches a maintainer-blocked area"); + expect(safe.detail).not.toContain("secret/private/**"); + expect(safe.action).not.toContain("secret/private/**"); + expect(safe.detail).toBe("Changed paths match maintainer-blocked areas."); + }); + + it("redacts the private test-expectation detail to a static phrase", () => { + const finding: FocusManifestFinding = { + code: "manifest_missing_tests", + severity: "warning", + title: "Maintainer test expectations unmet", + detail: "Maintainer expects test evidence: run the private fuzz suite; hit internal/regression.", + action: "Add or update tests for the private fuzz suite.", + }; + const safe = publicSafeManifestPolicyFinding(finding); + expect(safe.detail).not.toContain("private fuzz suite"); + expect(safe.action).not.toContain("private fuzz suite"); + expect(safe.detail).toBe("Maintainer test expectations are not satisfied by this PR."); + }); + + it("passes through a finding whose detail is already generic (no override)", () => { + const finding: FocusManifestFinding = { + code: "manifest_linked_issue_required", + severity: "warning", + title: "Maintainer requires a linked issue", + detail: "This repo's maintainer focus manifest requires every PR to reference a tracked issue.", + action: "Link the relevant issue (for example `Closes #123`) before opening the PR.", + }; + const safe = publicSafeManifestPolicyFinding(finding); + expect(safe.detail).toBe(finding.detail); + expect(safe.action).toBe(finding.action); + }); +});