diff --git a/src/services/agent-action-explanation-card.ts b/src/services/agent-action-explanation-card.ts index 2dfc728d0f..9230d0d1dc 100644 --- a/src/services/agent-action-explanation-card.ts +++ b/src/services/agent-action-explanation-card.ts @@ -8,7 +8,7 @@ type AgentActionExplanationInput = Pick< const BLOCKER_CATEGORY_ORDER: AgentActionBlockerCategory[] = ["branch", "account", "queue", "scoreability", "risk", "maintainer", "unknown"]; 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; + /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|private keys?|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; // Token alternatives stay local; the local-path alternatives compose from the canonical PUBLIC_LOCAL_PATH_INLINE // in redaction.ts (adds the previously-missed /root/ and /var/, plus the forward-slash Windows form C:/Users/). diff --git a/test/unit/agent-orchestrator.test.ts b/test/unit/agent-orchestrator.test.ts index 19f8347c68..71b7c330ef 100644 --- a/test/unit/agent-orchestrator.test.ts +++ b/test/unit/agent-orchestrator.test.ts @@ -804,6 +804,24 @@ describe("agent orchestrator", () => { expect(card.publicSafe.summary).not.toMatch(/\/root\/work|\/var\/log|C:\/Users\/alice/); }); + it("redacts 'private key' from the public-safe card, matching its sibling redaction lists (#8020)", () => { + // `private keys?` was present in 5 sibling public-redaction vocabularies (miner-dashboard-recommendations.ts:45, + // control-panel-roles.ts:297, decision-pack.ts:1438, weekly-value-report.ts:417, extension-contributor-context.ts:31) + // but missing here, so an App-credential reference leaked through publicSafe.whyNow unredacted. + const card = buildAgentActionExplanationCard({ + actionType: "choose_next_work", + status: "blocked", + why: ["Blocked by a private key rotation on the App credential."], + blockedBy: ["private key rotation pending"], + publicSafeSummary: "Resolve the App private key rotation before rerunning.", + safetyClass: "private", + }); + + expect(card.publicSafe.summary).not.toMatch(/private key/i); + expect(card.publicSafe.summary).toMatch(/private context/); + expect(card.publicSafe.whyNow).not.toMatch(/private key/i); + }); + it("does not split a surrogate pair when truncating a card field at the 300-character cap", () => { // A string is well-formed UTF-16 iff it has no lone surrogate (a high surrogate not followed by a // low one, or a low surrogate not preceded by a high one). Equivalent to String#isWellFormed without