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
3 changes: 2 additions & 1 deletion src/scenarios/input-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const scenarioSignalSources = [
] as const;
export type ScenarioSignalSource = (typeof scenarioSignalSources)[number];

const FORBIDDEN_PUBLIC_LANGUAGE =
/** Shared public-safety term set for scenario input + public summary guards (#8885 / #913). */
export const FORBIDDEN_PUBLIC_LANGUAGE =
/wallet|hotkey|coldkey|mnemonic|seed phrase|payout|estimated[-\s]?rewards?|rewards?|reward[-\s]?estimate|rankings?|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i;

const FORBIDDEN_SOURCE_UPLOAD_KEYS =
Expand Down
10 changes: 3 additions & 7 deletions src/scenarios/scenario-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { OpenPrPressureSimulation, OpenPrStrategyOption } from "../services
import type { ScoreGateBlocker } from "../scoring/preview";
import type { PendingPrScenarioDetection, OpenPrPendingClass } from "../scoring/pending-pr-scenarios";
import type { AgentScenarioInput } from "./input-model";
import { serializeScenarioInputPublic } from "./input-model";
import { FORBIDDEN_PUBLIC_LANGUAGE, serializeScenarioInputPublic } from "./input-model";

/**
* Public-safe rendering of scenario simulator outputs for MCP/API clients and
Expand Down Expand Up @@ -92,9 +92,6 @@ const PUBLIC_BLOCKER_TEXT: Partial<Record<ScoreGateBlocker["code"], string>> = {
stale_work: "Stale open PR(s) detected; consider closing stale work before opening more.",
};

const FORBIDDEN_PUBLIC_LANGUAGE =
/wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i;

function renderOptions(simulation: OpenPrPressureSimulation): RenderedScenarioOption[] {
return simulation.scenarios.map((s) => {
const rationaleParts = [...s.facts.slice(0, 1), ...s.tradeoffs.slice(0, 1)];
Expand Down Expand Up @@ -180,17 +177,16 @@ function extractDataClassification(scenarioInput: AgentScenarioInput | undefined
};
}

function assertPublicSummaryClean(summary: PublicScenarioSummary): void {
/** Defensive final guard: catches forbidden terms that slipped past per-field sanitization (#8885). */
export function assertPublicSummaryClean(summary: PublicScenarioSummary): void {
// Scan only rendered free-text fields. repoFullName/generatedAt are structural identifiers (the repo
// the summary is about), not sanitized content -- a legitimately named repo (e.g. "owner/hotkey-vault")
// must not make this guard throw and fail the whole summary.
const { repoFullName: _repoFullName, generatedAt: _generatedAt, ...renderedContent } = summary;
const serialized = JSON.stringify(renderedContent);
/* v8 ignore start -- Defensive: every rendered field is individually sanitized; this guards a future unsanitized field. */
if (FORBIDDEN_PUBLIC_LANGUAGE.test(serialized)) {
throw new Error("Public scenario summary still contains forbidden language.");
}
/* v8 ignore end */
}

/**
Expand Down
42 changes: 37 additions & 5 deletions test/unit/scenario-summary.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { describe, expect, it } from "vitest";
import { sanitizePublicComment } from "../../src/github/commands";
import { buildScenarioInput, createScenarioSignalEntry } from "../../src/scenarios/input-model";
import { renderPublicScenarioSummary } from "../../src/scenarios/scenario-summary";
import {
buildScenarioInput,
createScenarioSignalEntry,
FORBIDDEN_PUBLIC_LANGUAGE,
} from "../../src/scenarios/input-model";
import {
assertPublicSummaryClean,
renderPublicScenarioSummary,
type PublicScenarioSummary,
} from "../../src/scenarios/scenario-summary";
import { deriveEligibilityPlan } from "../../src/services/eligibility-plan";
import { simulateOpenPrPressure } from "../../src/services/open-pr-pressure-scenarios";
import type { PendingPrScenarioDetection } from "../../src/scoring/pending-pr-scenarios";
import { buildScorePreview, type ScoreGateBlocker } from "../../src/scoring/preview";
import type { QueueHealth, RoleContext } from "../../src/signals/engine";
import type { ScoringModelSnapshotRecord } from "../../src/types";

const FORBIDDEN_PUBLIC_LANGUAGE =
/wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i;

const snapshot: ScoringModelSnapshotRecord = {
id: "scenario-summary-model",
sourceKind: "test",
Expand Down Expand Up @@ -411,4 +416,31 @@ describe("renderPublicScenarioSummary", () => {
expect(summary.pendingScenarioNotes.join(" ")).not.toMatch(/Projected open PR count after pending cleanup/i);
expect(summary.pendingPullRequests[0]?.classification).toBe("custom pending class");
});

it("assertPublicSummaryClean rejects bare rewards/rankings like the input-model guard (#8885)", () => {
const base: PublicScenarioSummary = {
repoFullName: "octo/demo",
generatedAt: "2026-06-03T00:00:00.000Z",
advisoryOnly: true,
notAutonomousPrBot: true,
notPublicScoring: true,
headline: "Advisory scenario summary generated from available repo signals.",
options: [],
eligibilityNotes: [],
blockerNotes: [],
pendingScenarioNotes: [],
pendingPullRequests: [],
dataClassification: { facts: [], assumptions: [], unavailableSignals: [] },
};

expect(() => assertPublicSummaryClean({ ...base, headline: "Projected rewards look strong." })).toThrow(
/forbidden language/i,
);
expect(() => assertPublicSummaryClean({ ...base, eligibilityNotes: ["Private rankings leaked."] })).toThrow(
/forbidden language/i,
);
// Shared constant must stay identical to the input-model #913 term set.
expect(FORBIDDEN_PUBLIC_LANGUAGE.test("rewards")).toBe(true);
expect(FORBIDDEN_PUBLIC_LANGUAGE.test("rankings")).toBe(true);
});
});