From 85e2389669c047104480b89bb0ecaee4f69868f3 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:09:40 -0700 Subject: [PATCH] refactor(signals): shared public-safe redaction module (#542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift the private `isPublicSafeText` out of local-branch.ts into a shared `src/signals/redaction.ts` so every public surface (PR/issue comments, check annotations, notifications, badge, extension payloads, slop/advisory reasons) filters through one canonical regex instead of re-deriving its own and drifting. - New `src/signals/redaction.ts` exports `isPublicSafeText` + a reusable `PUBLIC_UNSAFE_PATTERN` constant. The pattern is byte-for-byte the same and intentionally NON-global, so `.test()` stays stateless. - local-branch.ts imports it; all 8 call sites unchanged. No behavior change — the existing local-branch redaction tests stay green. - Added test/unit/redaction.test.ts exercising the boundary directly (economic/identity signals + local paths rejected; clean text accepted; guards the non-global/stateless requirement). Closes #542. --- src/signals/local-branch.ts | 5 +--- src/signals/redaction.ts | 16 +++++++++++ test/unit/redaction.test.ts | 53 +++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/signals/redaction.ts create mode 100644 test/unit/redaction.test.ts diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index eb12febc68..dc7ca7dba6 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -19,6 +19,7 @@ import { buildRepoRewardRisk, type RepoRewardRisk, type RewardRiskAction } from import { buildLocalWorkspaceIntelligence, type LocalWorkspaceIntelligence } from "./local-workspace-intelligence"; import { buildFocusManifestGuidance, parseFocusManifest, type FocusManifestGuidance } from "./focus-manifest"; import { sanitizeLocalScorerWarnings } from "./local-scorer-diagnostics"; +import { isPublicSafeText } from "./redaction"; import { deriveEligibilityPlan } from "../services/eligibility-plan"; import { scenarioInputFromLocalBranchMetadata } from "../scenarios/input-model"; import { renderPublicScenarioSummary, type PublicScenarioSummary, type ScenarioSummaryInput } from "../scenarios/scenario-summary"; @@ -1201,10 +1202,6 @@ function firstCommitTitle(messages: string[] | undefined): string | undefined { return messages?.find((message) => message.trim().length > 0)?.split("\n")[0]?.trim() || undefined; } -function isPublicSafeText(text: string): boolean { - return !/\b(reward\w*|score\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i.test(text); -} - function safeRepoPath(path: string): string { /* v8 ignore next -- Empty path fallback protects malformed local-git adapters; path redaction is covered by local branch tests. */ return /^(\/Users\/|\/home\/|\/tmp\/|[A-Z]:\/Users\/)/i.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/"); diff --git a/src/signals/redaction.ts b/src/signals/redaction.ts new file mode 100644 index 0000000000..c2b40c0f87 --- /dev/null +++ b/src/signals/redaction.ts @@ -0,0 +1,16 @@ +// #542: the canonical public/private boundary primitive. Any text destined for a PUBLIC surface — PR/issue +// comments, check annotations, notifications, badge, extension payloads, slop/advisory reasons — must pass +// `isPublicSafeText` first, so a single regex governs redaction and new surfaces cannot drift their own copy. +// +// It rejects gittensor economic/identity signals (rewards, raw/trust score, wallet/hotkey/coldkey/mnemonic, +// farming, payout, ranking, (private) reviewability) and local filesystem paths. +// +// The pattern is intentionally NON-GLOBAL so `.test()` stays stateless (no `lastIndex` carry-over between +// calls) and the exported constant can be reused safely across call sites and modules. +export const PUBLIC_UNSAFE_PATTERN = + /\b(reward\w*|score\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i; + +/** True iff `text` contains nothing that must stay private — i.e. it is safe to surface on a public GitHub surface. */ +export function isPublicSafeText(text: string): boolean { + return !PUBLIC_UNSAFE_PATTERN.test(text); +} diff --git a/test/unit/redaction.test.ts b/test/unit/redaction.test.ts new file mode 100644 index 0000000000..6be22518b1 --- /dev/null +++ b/test/unit/redaction.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { isPublicSafeText, PUBLIC_UNSAFE_PATTERN } from "../../src/signals/redaction"; + +describe("isPublicSafeText (#542 shared public/private boundary)", () => { + it("accepts text with no private signals", () => { + expect(isPublicSafeText("Add a retry to the cache reconnect path.")).toBe(true); + expect(isPublicSafeText("- PR #12: changes requested.")).toBe(true); + expect(isPublicSafeText("")).toBe(true); + }); + + it("rejects gittensor economic / identity signals", () => { + for (const text of [ + "estimated reward is high", + "your score will rise", + "wallet 5F...", + "hotkey leaked", + "coldkey backup", + "mnemonic phrase", + "this looks like farming", + "payout pending", + "ranking change", + "raw trust value", + "raw-trust score", + "trust_score 0.8", + "private reviewability internals", + "reviewability breakdown", + ]) { + expect(isPublicSafeText(text)).toBe(false); + } + }); + + it("rejects local filesystem paths (posix and Windows)", () => { + expect(isPublicSafeText("/Users/alice/project")).toBe(false); + expect(isPublicSafeText("/home/bob/repo")).toBe(false); + expect(isPublicSafeText("/tmp/scratch")).toBe(false); + expect(isPublicSafeText("C:\\Users\\carol\\repo")).toBe(false); + expect(isPublicSafeText("C:/Users/carol/repo")).toBe(false); + }); + + it("is case-insensitive", () => { + expect(isPublicSafeText("WALLET")).toBe(false); + expect(isPublicSafeText("Payout")).toBe(false); + }); + + it("uses a NON-global pattern so .test() is stateless (no lastIndex carry-over)", () => { + expect(PUBLIC_UNSAFE_PATTERN.global).toBe(false); + // A global regex would alternate true/false across repeated .test() calls on the same input. + expect(PUBLIC_UNSAFE_PATTERN.test("wallet")).toBe(true); + expect(PUBLIC_UNSAFE_PATTERN.test("wallet")).toBe(true); + expect(isPublicSafeText("clean line")).toBe(true); + expect(isPublicSafeText("clean line")).toBe(true); + }); +});