Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 });
Expand Down
35 changes: 27 additions & 8 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -225,6 +225,31 @@ export async function runRetentionPrune(env: Env, requestedBy: string, dryRun: b
});
}

const PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES: Partial<Record<FocusManifestFinding["code"], Pick<AdvisoryFinding, "detail" | "action">>> = {
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.",
},
};

export function publicSafeManifestPolicyFinding(finding: FocusManifestFinding): AdvisoryFinding {
return {
code: finding.code,
severity: finding.severity,
title: finding.title,
detail: finding.detail,
/* v8 ignore next -- the three manifest policy findings always carry an action; the no-action arm is unreachable. */
...(finding.action !== undefined ? { action: finding.action } : {}),
// Override the leaky detail/action with a static, public-safe version for the codes whose raw text would echo
// private blocked-path globs / test expectations; codes absent from the table keep their already-generic text.
...PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES[finding.code],
};
}

export async function processJob(env: Env, message: JobMessage): Promise<void> {
switch (message.type) {
case "refresh-registry":
Expand Down Expand Up @@ -2616,13 +2641,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
Expand Down
50 changes: 43 additions & 7 deletions test/unit/mcp-predict-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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({
Expand Down Expand Up @@ -50,7 +51,8 @@ describe("MCP gittensory_predict_gate", () => {
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({
Expand All @@ -66,6 +68,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
Expand All @@ -88,7 +124,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);

Expand All @@ -108,7 +144,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);

Expand All @@ -127,7 +163,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
Expand All @@ -154,7 +190,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 });

Expand Down
51 changes: 51 additions & 0 deletions test/unit/public-safe-manifest-finding.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading