diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 1d8e7a55d4..02f2c77ef5 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -162,6 +162,7 @@ import { DEFAULT_COMMAND_AUTHORIZATION_POLICY, normalizeCommandAuthorizationPoli import { normalizeContributorBlacklist } from "../settings/contributor-blacklist"; import { normalizeAutonomyPolicy, normalizeAutoMaintainPolicy, DEFAULT_AUTO_MAINTAIN_POLICY } from "../settings/autonomy"; import { decryptSecret, encryptSecret, sha256Hex } from "../utils/crypto"; +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; import { jsonString, nowIso, parseJson, repoParts } from "../utils/json"; const MAX_STORED_BODY_CHARS = 4000; @@ -5210,7 +5211,6 @@ const PRODUCT_USAGE_SENSITIVE_KEY = /authorization|cookie|token|secret|password|private[_-]?key|source|body|diff|patch|prompt|raw[_-]?trust|trust[_-]?score|wallet|hotkey|coldkey|seed|mnemonic|local[_-]?path|repo[_-]?root|cwd|scoreability|reviewability|farming/i; const PRODUCT_USAGE_SENSITIVE_VALUE = /\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|scoreability|reviewability|farming|reward estimate|payout)\b/i; -const PRODUCT_USAGE_LOCAL_PATH = /(?:\/Users|\/home|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g; const PRODUCT_USAGE_TOKEN_VALUE = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; const PRODUCT_USAGE_BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi; @@ -5255,7 +5255,7 @@ function sanitizeProductUsageJson(value: unknown, depth: number, actorRedactor: function sanitizeProductUsageString(value: string, maxLength: number): string { const redacted = value - .replace(PRODUCT_USAGE_LOCAL_PATH, "") + .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") .replace(PRODUCT_USAGE_TOKEN_VALUE, "") .replace(PRODUCT_USAGE_BEARER_VALUE, "Bearer "); if (PRODUCT_USAGE_SENSITIVE_VALUE.test(redacted)) return ""; diff --git a/src/services/agent-action-explanation-card.ts b/src/services/agent-action-explanation-card.ts index dea2261fda..0a29eeb86a 100644 --- a/src/services/agent-action-explanation-card.ts +++ b/src/services/agent-action-explanation-card.ts @@ -1,4 +1,5 @@ import type { AgentActionBlockerCategory, AgentActionExplanationCard, AgentActionRecord } from "../types"; +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; type AgentActionExplanationInput = Pick< AgentActionRecord, @@ -9,7 +10,7 @@ const BLOCKER_CATEGORY_ORDER: AgentActionBlockerCategory[] = ["branch", "account const PUBLIC_FORBIDDEN_PATTERN = /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|raw[-_\s]?trust scores?|trust scores?|private reviewability|reviewability internals?|private scoreability|scoreability|projected scores?|score(?:d|s|ability)?|public score estimates?|estimated scores?|score estimates?|score previews?|reward estimates?|payouts?|farming|reward optimization|private rankings?)\b/gi; const PUBLIC_SCORE_DELTA_PATTERN = /\b(?:projected\s+)?score\w*(?:\s+\w+){0,4}\s+[-+]?\d+(?:\.\d+)?\s*->\s*[-+]?\d+(?:\.\d+)?\b/gi; -const TOKEN_OR_PATH_PATTERN = /\bgithub_pat_[A-Za-z0-9_]+|\bgh[pousr]_[A-Za-z0-9_]+|\/Users\/\S+|\/home\/\S+|\/tmp\/\S+|[A-Z]:\\Users\\\S+/gi; +const TOKEN_PATTERN = /\bgithub_pat_[A-Za-z0-9_]+|\bgh[pousr]_[A-Za-z0-9_]+/gi; export function withAgentActionExplanationCard(action: AgentActionRecord): AgentActionRecord { return { ...action, explanationCard: buildAgentActionExplanationCard(action) }; @@ -118,7 +119,8 @@ function categorizeBlocker(blocker: string): AgentActionBlockerCategory { function sanitizePublicCardText(value: string): string { return compactText(value) - .replace(TOKEN_OR_PATH_PATTERN, "") + .replace(TOKEN_PATTERN, "") + .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "") .replace(PUBLIC_SCORE_DELTA_PATTERN, "private context") .replace(PUBLIC_FORBIDDEN_PATTERN, "private context") .replace(/private context(?:[,\s]+private context)+/gi, "private context") diff --git a/src/services/control-panel-roles.ts b/src/services/control-panel-roles.ts index a793860791..05efc43d39 100644 --- a/src/services/control-panel-roles.ts +++ b/src/services/control-panel-roles.ts @@ -1,6 +1,7 @@ import { isAuthorizedGitHubSessionLogin } from "../auth/security"; import { getFreshOfficialMinerDetection, getRepository, listAllPullRequests, listInstallations, listRepositories } from "../db/repositories"; import type { ControlPanelRoleCard, ControlPanelRoleName, ControlPanelRoleSummary, InstallationRecord, PullRequestRecord, RepositoryRecord } from "../types"; +import { redactPublicLocalPaths } from "../signals/redaction"; import { nowIso } from "../utils/json"; export type RoleSummaryInputs = { @@ -289,8 +290,7 @@ function isMaintainerAssociation(value: string | null | undefined): boolean { } export function sanitizeRoleText(value: string): string { - const redacted = value - .replace(/(?:\/Users|\/home|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g, "") + const redacted = redactPublicLocalPaths(value, "") .replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if (/\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|payout|reward estimate|farming|private reviewability|public score estimate)\b/i.test(redacted)) return ""; diff --git a/src/services/miner-dashboard-recommendations.ts b/src/services/miner-dashboard-recommendations.ts index c2fd49f503..b8a5ff6abc 100644 --- a/src/services/miner-dashboard-recommendations.ts +++ b/src/services/miner-dashboard-recommendations.ts @@ -1,4 +1,5 @@ import type { ContributorDecisionPack } from "./decision-pack"; +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; import type { SignalSnapshotRecord } from "../types"; export type MinerDashboardSignalGroup = "repo_state" | "contributor_state" | "validation_state" | "policy_context"; @@ -42,7 +43,6 @@ const CHANGE_LABEL_LIMIT = 6; const REASON_LIMIT = 3; const FORBIDDEN_PUBLIC_TEXT = /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|private keys?|raw[-_\s]?trust(?: scores?)?|trust[-_\s]?scores?|reward(?:[-_\s]?(?:estimate|prediction|claim|score))?s?|payouts?|farming(?:[-_\s]?language)?|private[-_\s]?reviewability|private[-_\s]?scoreability|scoreability|public[-_\s]?score[-_\s]?(?:estimate|prediction)|estimated[-_\s]?score|score[-_\s]?estimate)\b/gi; -const LOCAL_PATH = /(?:\/(?:Users|home|root|tmp|var)\/[^\s,;:)]+|[A-Za-z]:\\Users\\[^\s,;:)]+)/g; const FORBIDDEN_TOKEN = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g; export function previousDecisionPackFromSnapshots(currentPack: ContributorDecisionPack, snapshots: SignalSnapshotRecord[]): ContributorDecisionPack | undefined { @@ -383,7 +383,7 @@ function numberValue(record: DashboardRecord | undefined, key: string): number | function sanitizePublicText(value: string): string { return value - .replace(LOCAL_PATH, "[local path]") + .replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "[local path]") .replace(FORBIDDEN_TOKEN, "private context") .replace(FORBIDDEN_PUBLIC_TEXT, "private context") .replace(/\s+/g, " ") diff --git a/src/services/weekly-value-report.ts b/src/services/weekly-value-report.ts index ed39b43296..3921101a75 100644 --- a/src/services/weekly-value-report.ts +++ b/src/services/weekly-value-report.ts @@ -12,6 +12,7 @@ import { } from "../db/repositories"; import { getLatestRegistrySnapshot } from "../registry/sync"; import { loadUpstreamStatus, type UpstreamStatus } from "../upstream/ruleset"; +import { redactPublicLocalPaths } from "../signals/redaction"; import type { InstallationHealthRecord, InstallationRecord, @@ -408,8 +409,7 @@ function normalizeReportDays(value: number | null | undefined): number { } function sanitizeReportText(value: string): string { - const redacted = value - .replace(/(?:\/Users|\/home|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g, "") + const redacted = redactPublicLocalPaths(value, "") .replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "") .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer "); if ( diff --git a/src/signals/extension-contributor-context.ts b/src/signals/extension-contributor-context.ts index 4fa823960c..170d97b522 100644 --- a/src/signals/extension-contributor-context.ts +++ b/src/signals/extension-contributor-context.ts @@ -1,4 +1,5 @@ import type { ContributorOpportunity, PublicReadinessScore } from "./engine"; +import { redactPublicLocalPaths } from "./redaction"; // ─── Contributor-context payloads for the browser extension (#556) ─────────────────────────────── // The contributor (miner) side of the extension overlay. Every payload here is PUBLIC-SAFE and self- @@ -25,7 +26,10 @@ const FORBIDDEN_EXTENSION_TERMS = /\b(?:rewards?|payouts?|farming|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|score[-\s]?(?:estimate|preview|prediction)s?|estimated[-\s]?scores?|scoreability|private[-\s]?reviewability|reviewability[-\s]?internals?|private[-\s]?rankings?)\b/gi; export function redactExtensionText(text: string): string { - return text.replace(FORBIDDEN_EXTENSION_TERMS, "[redacted]").replace(/\s+/g, " ").trim(); + return redactPublicLocalPaths(text, "[redacted]") + .replace(FORBIDDEN_EXTENSION_TERMS, "[redacted]") + .replace(/\s+/g, " ") + .trim(); } // ── issue-fit: "is this issue a good one for me to pick up?" ────────────────────────────────────── diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 31c010aa52..002d5e734c 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -1,3 +1,4 @@ +import { PUBLIC_LOCAL_PATH_INLINE } from "./redaction"; import { parse as parseYaml } from "yaml"; import type { GatePolicyPack, GateRuleMode, JsonValue, RepositorySettings } from "../types"; import { normalizeAutonomyPolicy, normalizeAutoMaintainPolicy } from "../settings/autonomy"; @@ -240,8 +241,13 @@ const EMPTY_MANIFEST: FocusManifest = { * Public-safe redaction guard shared with the local-branch packet renderer. Public manifest * text must not leak reward, wallet/key, ranking, or local filesystem path material. */ +const FOCUS_MANIFEST_PUBLIC_UNSAFE = new RegExp( + String.raw`\b(reward\w*|score\w*|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|farming|payouts?|rankings?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|private[-\s]?reviewability|reviewability(?:[-\s]?internals?)?|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)s?|estimated[-\s]?scores?|score[-\s]?(?:estimate|prediction|preview)s?)\b|${PUBLIC_LOCAL_PATH_INLINE}`, + "i", +); + export function isFocusManifestPublicSafe(text: string): boolean { - return !/\b(reward\w*|score\w*|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|farming|payouts?|rankings?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|private[-\s]?reviewability|reviewability(?:[-\s]?internals?)?|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)s?|estimated[-\s]?scores?|score[-\s]?(?:estimate|prediction|preview)s?)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:\\Users\\/i.test(text); + return !FOCUS_MANIFEST_PUBLIC_UNSAFE.test(text); } function emptyManifest(source: FocusManifestSource, warnings: string[] = []): FocusManifest { diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index a417babbaf..a1837633db 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -21,7 +21,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 { isPublicSafeText, PUBLIC_LOCAL_PATH_PREFIX_PATTERN } from "./redaction"; import { deriveEligibilityPlan } from "../services/eligibility-plan"; import { scenarioInputFromLocalBranchMetadata } from "../scenarios/input-model"; import { renderPublicScenarioSummary, type PublicScenarioSummary, type ScenarioSummaryInput } from "../scenarios/scenario-summary"; @@ -1233,7 +1233,7 @@ function firstCommitTitle(messages: string[] | undefined): string | undefined { 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\/|\/root\/|\/tmp\/|[A-Z]:\/Users\/)/i.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/"); + return PUBLIC_LOCAL_PATH_PREFIX_PATTERN.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/"); } export function isTestFile(file: string): boolean { diff --git a/src/signals/redaction.ts b/src/signals/redaction.ts index d24e8f02e1..1f34f9520a 100644 --- a/src/signals/redaction.ts +++ b/src/signals/redaction.ts @@ -22,7 +22,38 @@ // intentionally NOT collapsed onto `PUBLIC_UNSAFE_TERMS`. export const PUBLIC_UNSAFE_TERMS = String.raw`(?:reward|score|wallet|hotkey|coldkey|mnemonic|payout|ranking)\w*|farming|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability`; -export const PUBLIC_UNSAFE_PATTERN = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b|/Users/|/home/|/root/|/tmp/|[A-Z]:[\\/]Users[\\/]`, "i"); +/** Posix local path roots that must not appear on public surfaces. */ +export const PUBLIC_LOCAL_PATH_ROOTS = String.raw`/Users/|/home/|/root/|/tmp/|/var/`; + +/** Windows user-profile paths that must not appear on public surfaces. */ +export const PUBLIC_LOCAL_PATH_WINDOWS = String.raw`[A-Z]:[\\/]Users[\\/]`; + +/** Inline alternation for composing boundary patterns (non-global). */ +export const PUBLIC_LOCAL_PATH_INLINE = `${PUBLIC_LOCAL_PATH_ROOTS}|${PUBLIC_LOCAL_PATH_WINDOWS}`; + +/** Prefix test for absolute changed-file paths (anchored at start). */ +export const PUBLIC_LOCAL_PATH_PREFIX_PATTERN = new RegExp( + String.raw`^(\/Users\/|\/home\/|\/root\/|\/tmp\/|\/var\/|[A-Z]:\/Users\/)`, + "i", +); + +/** Global scrubber for known local path roots in free-form text. */ +export const PUBLIC_LOCAL_PATH_SCRUB_PATTERN = new RegExp( + String.raw`(?:\/Users|\/home|\/root|\/tmp|\/var)\/[^\s"',;:)]*|[A-Za-z]:\\Users\\[^\s"',;)]*`, + "g", +); + +export const PUBLIC_UNSAFE_PATTERN = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b|${PUBLIC_LOCAL_PATH_INLINE}`, "i"); + +/** True when `text` contains a known local filesystem path root. */ +export function containsPublicLocalPath(text: string): boolean { + return new RegExp(PUBLIC_LOCAL_PATH_INLINE, "i").test(text); +} + +/** Replace known local filesystem path roots with `replacement`. */ +export function redactPublicLocalPaths(text: string, replacement = ""): string { + return text.replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, replacement); +} /** 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 { diff --git a/test/unit/extension-contributor-context.test.ts b/test/unit/extension-contributor-context.test.ts index ada9c8214a..e1536ee0d9 100644 --- a/test/unit/extension-contributor-context.test.ts +++ b/test/unit/extension-contributor-context.test.ts @@ -59,6 +59,11 @@ describe("redactExtensionText", () => { it("leaves safe text untouched", () => { expect(redactExtensionText("Maintainer-created issue, good fit.")).toBe("Maintainer-created issue, good fit."); }); + + it("redacts local filesystem paths", () => { + expect(redactExtensionText("Evidence from /root/work/src/cache.ts")).toBe("Evidence from [redacted]"); + expect(redactExtensionText("cache at /var/tmp/build")).toBe("cache at [redacted]"); + }); }); describe("buildExtensionIssueFit", () => { diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 316e7682a5..9b4ac31563 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -690,6 +690,8 @@ describe("public-safe invariant", () => { expect(isFocusManifestPublicSafe("Keep PRs focused")).toBe(true); expect(isFocusManifestPublicSafe("estimate your reward")).toBe(false); expect(isFocusManifestPublicSafe("paste your hotkey")).toBe(false); + expect(isFocusManifestPublicSafe("build from /root/work/repo")).toBe(false); + expect(isFocusManifestPublicSafe("cache under /var/tmp/build")).toBe(false); }); it("never emits public next steps that contain forbidden language for generated manifests", () => { diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index ec7667d7da..79aa177fbe 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1236,6 +1236,28 @@ describe("local branch analysis", () => { expect(analysis.prPacket.markdown).not.toContain("/root/work"); }); + it("hides /var paths from public PR packet changed paths", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + body: "Fixes #7", + changedFiles: [{ path: "/var/tmp/build/src/cache.ts", additions: 12, deletions: 2, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.prPacket.markdown).toContain("[local path hidden]"); + expect(analysis.prPacket.markdown).not.toContain("/var/tmp/build"); + }); + it("removes snake_case private signals from public PR packet markdown", () => { const analysis = buildLocalBranchAnalysis({ input: { diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index d22d700649..826027adb0 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -132,6 +132,12 @@ describe("sanitizeRoleText path redaction", () => { expect(sanitizeRoleText("/tmp/deploy_key.pem")).toBe(""); }); + it("redacts root and var home paths entirely", () => { + expect(sanitizeRoleText("/root/work/repo")).toBe(""); + expect(sanitizeRoleText("/var/tmp/build/cache")).toBe(""); + expect(sanitizeRoleText("clone /root/work/repo here")).toBe("clone here"); + }); + it("redacts Windows C:\\Users paths entirely", () => { expect(sanitizeRoleText("C:\\Users\\bob\\AppData\\token.txt")).toBe(""); }); diff --git a/test/unit/redaction.test.ts b/test/unit/redaction.test.ts index 05c28e87cb..b050bcf900 100644 --- a/test/unit/redaction.test.ts +++ b/test/unit/redaction.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "vitest"; -import { isPublicSafeText, PUBLIC_UNSAFE_PATTERN } from "../../src/signals/redaction"; +import { + containsPublicLocalPath, + isPublicSafeText, + PUBLIC_LOCAL_PATH_PREFIX_PATTERN, + PUBLIC_UNSAFE_PATTERN, + redactPublicLocalPaths, +} from "../../src/signals/redaction"; describe("isPublicSafeText (#542 shared public/private boundary)", () => { it("accepts text with no private signals", () => { @@ -41,6 +47,7 @@ describe("isPublicSafeText (#542 shared public/private boundary)", () => { expect(isPublicSafeText("/root/project/src")).toBe(false); expect(isPublicSafeText("clone failed at /root/work/repo")).toBe(false); expect(isPublicSafeText("/tmp/scratch")).toBe(false); + expect(isPublicSafeText("/var/log/app/build.log")).toBe(false); expect(isPublicSafeText("C:\\Users\\carol\\repo")).toBe(false); expect(isPublicSafeText("C:/Users/carol/repo")).toBe(false); }); @@ -59,3 +66,19 @@ describe("isPublicSafeText (#542 shared public/private boundary)", () => { expect(isPublicSafeText("clean line")).toBe(true); }); }); + +describe("shared public local-path helpers", () => { + it("detects and redacts known local path roots", () => { + expect(containsPublicLocalPath("/root/work/repo")).toBe(true); + expect(containsPublicLocalPath("/var/folders/ci/cache")).toBe(true); + expect(containsPublicLocalPath("owner/repo")).toBe(false); + expect(redactPublicLocalPaths("clone /root/work/repo here")).toBe("clone here"); + expect(redactPublicLocalPaths("cache at /var/tmp/build")).toBe("cache at "); + }); + + it("matches absolute changed-file prefixes for safeRepoPath", () => { + expect(PUBLIC_LOCAL_PATH_PREFIX_PATTERN.test("/root/work/src/app.ts")).toBe(true); + expect(PUBLIC_LOCAL_PATH_PREFIX_PATTERN.test("/var/lib/cache.ts")).toBe(true); + expect(PUBLIC_LOCAL_PATH_PREFIX_PATTERN.test("src/app.ts")).toBe(false); + }); +});