Skip to content

Preserve allowlisted mentions in add_comment sanitization (@copilot regression)#32683

Merged
pelikhan merged 4 commits into
mainfrom
copilot/review-add-comment-sanitization
May 16, 2026
Merged

Preserve allowlisted mentions in add_comment sanitization (@copilot regression)#32683
pelikhan merged 4 commits into
mainfrom
copilot/review-add-comment-sanitization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 16, 2026

add_comment was re-sanitizing comment bodies with a local allowlist that omitted workflow-level mention policy, so @copilot was escaped even when explicitly allowed (e.g., PR Sous Chef). This change aligns handler-time sanitization with configured safe-output mention policy.

  • Config propagation

    • Pass top-level mentions configuration into the add_comment handler via safe_output_handler_manager, instead of limiting handler context to add_comment-scoped fields.
  • Allowlist merge in add_comment

    • Build allowedAliases from:
      • parent entity authors (existing behavior), and
      • configured mentions.allowed entries (new behavior).
    • Deduplicate case-insensitively while preserving first-seen form.
  • Alias normalization

    • Normalize configured entries so both copilot and @copilot resolve to the same alias before sanitization.
  • Behavioral effect

    • Default remains unchanged (mentions are escaped unless allowed).
    • When workflow policy allows Copilot, @copilot remains unescaped in posted comments.
// before
processedBody = sanitizeContent(processedBody, { allowedAliases: parentAuthors });

// after
const allowedMentionAliases = mergeUniqueAliases(parentAuthors, config.mentions?.allowed);
processedBody = sanitizeContent(processedBody, { allowedAliases: allowedMentionAliases });

Copilot AI and others added 3 commits May 16, 2026 18:48
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Preserve allowed @copilot mentions in add_comment sanitization Preserve allowlisted mentions in add_comment sanitization (@copilot regression) May 16, 2026
Copilot AI requested a review from pelikhan May 16, 2026 18:54
@pelikhan pelikhan marked this pull request as ready for review May 16, 2026 18:57
Copilot AI review requested due to automatic review settings May 16, 2026 18:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression where the add_comment handler re-sanitized comment bodies using a narrower mention allowlist than the workflow-level safe-outputs mention policy, causing allowlisted mentions (notably @copilot) to be escaped.

Changes:

  • Propagate top-level mentions policy into the add_comment handler via safe_output_handler_manager.
  • Merge configured mention allowlist entries with parent entity authors when building allowedAliases for sanitizeContent, with @-prefix normalization and case-insensitive dedupe.
  • Add a regression test ensuring @copilot remains unescaped when explicitly allowed.
Show a summary per file
File Description
actions/setup/js/safe_output_handler_manager.cjs Passes workflow-level mentions config into the add_comment handler config.
actions/setup/js/add_comment.cjs Merges configured allowed mention aliases with parent authors before the second sanitization pass.
actions/setup/js/add_comment.test.cjs Adds a regression test for preserving @copilot when allowlisted.
.github/workflows/workflow-health-manager.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/weekly-blog-post-writer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/technical-doc-writer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/smoke-ci.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/sergo.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/security-compliance.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/pr-triage-agent.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/metrics-collector.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/glossary-maintainer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/firewall-escape.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/discussion-task-miner.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/developer-docs-consolidator.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/delight.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/deep-report.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-testify-uber-super-expert.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-sentrux-report.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-news.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-community-attribution.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-code-metrics.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-cli-performance.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/daily-agent-of-the-day-blog-writer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-token-optimizer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-token-audit.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-session-insights.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-pr-prompt-analysis.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-pr-nlp-analysis.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-cli-deep-research.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/copilot-agent-analysis.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/audit-workflows.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).
.github/workflows/agent-performance-analyzer.lock.yml Updates GH_AW_MEMORY_CONSTRAINTS text (max patch size “max” value).

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 33/33 changed files
  • Comments generated: 3

Comment thread actions/setup/js/add_comment.cjs Outdated
Comment on lines +361 to +365
@@ -362,6 +362,7 @@ async function main(config = {}) {
const maxCount = config.max || 20;
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
const includeFooter = parseBoolTemplatable(config.footer, true);
const configuredMentionAliases = Array.isArray(config.mentions?.allowed) ? config.mentions.allowed.map(alias => (typeof alias === "string" ? alias.trim().replace(/^@+/, "") : "")).filter(alias => alias.length > 0) : [];
Comment on lines +302 to +306
// Pass top-level mentions policy through to add_comment so the handler can
// preserve the same allowed mention aliases used during collection.
if (type === "add_comment" && handlerConfig.mentions == null && config.mentions != null) {
handlerConfig.mentions = config.mentions;
}
Comment on lines +281 to +284
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools'
GH_AW_MEMORY_BRANCH_NAME: 'memory/meta-orchestrators'
GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: **\n- **Max File Size**: 102400 bytes (0.10 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 51200 bytes (50 KB) total per push (max: 100 KB)\n"
GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: **\n- **Max File Size**: 102400 bytes (0.10 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 51200 bytes (50 KB) total per push (max: 1024 KB)\n"
@github-actions github-actions Bot mentioned this pull request May 16, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnose and /tdd — this is a targeted bug fix with a regression test, which maps squarely to those two skills.

Key Themes

  • Test coverage gaps: The regression test covers the happy path (@copilot with @ prefix) but misses (a) the no-@ normalization path and (b) the deduplication path when parentAuthors and mentions.allowed overlap — both of which have dedicated logic in the implementation.
  • mergeUniqueAliases abstraction not extracted: The PR description describes the fix as a call to mergeUniqueAliases(parentAuthors, config.mentions?.allowed), but the implementation inlines the two-loop dedupe. A named helper would be directly unit-testable and make the call site self-documenting.
  • Config propagation generality: The type === "add_comment" guard in safe_output_handler_manager.cjs is correct for now but creates an inconsistency that may need to be repeated for future handlers.

Positive Highlights

  • ✅ Root cause is properly addressed at the right layer — config propagation from safe_output_handler_manager rather than a band-aid inside the handler itself.
  • ✅ Alias normalization (replace(/^@+/, "")) and case-insensitive deduplication are both correct and handle real-world input variance.
  • ✅ Regression test follows the established eval-based test pattern and clearly demonstrates the before/after behaviour.
  • ✅ PR description is detailed and includes a clear before/after code snippet — easy to understand the intent.

Verdict

No blocking issues — the fix is correct and the regression test is meaningful. Suggestions above are improvements to test coverage and a modest refactor opportunity; none are required for merge.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 6.7M

type: "add_comment",
body: "@copilot review all comments",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new test only exercises @copilot (with the @ prefix), but the normalization in add_comment.cjs also strips leading @ so both "@copilot" and "copilot" should resolve to the same alias. A complementary test case using mentions: { allowed: ["copilot"] } (no @) would lock down the normalization path and prevent a future regression if the .replace(/^@+/, "") logic changes.

it("should preserve `@copilot` mention when allowlist entry omits @ prefix", async () => {
  // ... same setup ...
  const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { allowed: ["copilot"] } }); })()`)
  const result = await handler({ type: "add_comment", body: "`@copilot` review" }, {})
  expect(capturedBody).toContain("`@copilot`")
  expect(capturedBody).not.toContain("`@copilot`")
})

expect(capturedBody).toContain("@copilot");
expect(capturedBody).not.toContain("`@copilot`");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The deduplication path (where an alias appears in both parentAuthors and mentions.allowed) isn't exercised. If parentAuthors already contains "copilot" and mentions.allowed also lists "@copilot", the merged list should still contain only one @copilot in the sanitized output. A test covering this would lock down the seenAllowedMentionAliases Set logic:

it("should not duplicate alias when it appears in both parentAuthors and mentions.allowed", async () => {
  // payload where PR author is also in mentions.allowed
  mockContext.payload = {
    pull_request: { number: 1, user: { login: "copilot", type: "User" } },
  }
  const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { allowed: ["`@copilot`"] } }); })()`)
  const result = await handler({ type: "add_comment", body: "`@copilot` thanks" }, {})
  // `@copilot` should appear exactly once, not escaped
  expect(capturedBody.match(/`@copilot/g`)).toHaveLength(1)
})

@@ -362,6 +362,7 @@ async function main(config = {}) {
const maxCount = config.max || 20;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The PR description references a mergeUniqueAliases helper but the implementation inlines the two-loop deduplication instead. The inline approach works correctly, but the logic in add_comment.cjs (lines 562–585) is non-trivial enough that a named, exported helper would:

  1. Enable isolated unit tests for the merge/dedupe logic without spinning up the full handler
  2. Make the call site at line 590 read like the description's pseudocode

Not blocking — just noting the gap between the stated design and what landed.

// Call the factory function with config to get the message handler
const handlerConfig = { ...(config[type] || {}) };

// Pass top-level mentions policy through to add_comment so the handler can
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The type === "add_comment" guard is a targeted fix that works for the immediate regression, but config propagation is now inconsistent: add_comment gets mentions forwarded; other handlers that may grow similar needs in the future won't. An alternative that generalises without requiring per-type patches:

// Merge top-level keys that the handler config doesn't already override
const handlerConfig = { ...config, ...(config[type] || {}) };

This follows the same shallow-merge pattern already used elsewhere and removes the need for the handler-type guard. Worth considering if a second handler ever needs a top-level field.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 70/100

⚠️ Acceptable — with suggestions

Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 0 (0%)
Duplicate test clusters 0
Test inflation detected No (1.57:1)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
should preserve mention when allowlist includes copilot add_comment.test.cjs:2434 ✅ Design Happy-path only; no error/edge cases

Flagged Tests — Suggestions

⚠️ should preserve mention when allowlist includes copilot (add_comment.test.cjs:2434)

Classification: Design test — behavioral contract ✅
Issue: Happy-path only. The test verifies that a valid allowlisted mention is preserved, but does not cover complementary edge cases:

  • Empty allowlist (mentions: { allowed: [] }) — should still escape the mention
  • Case sensitivityallowed: ["Copilot"] vs lowercase
  • Partial match"copilots" (plural) should not be preserved when only "copilot" is allowlisted
  • Invalid entryallowed: [null] or allowed: ["@"]

What design invariant does this test enforce? That an explicitly allowlisted mention survives the sanitizer without being backtick-escaped — the core regression guard for this PR.

What would break if deleted? A future refactor re-escaping allowlisted mentions would go undetected.

Suggested improvement: Convert to a table-driven it.each covering both escaping and non-escaping cases, replacing the existing should neutralize ... by default test to reduce duplication.


Language Support

  • 🐹 Go: 0 tests
  • 🟨 JavaScript (*.test.cjs): 1 test (vitest)

Verdict

Check passed. 0% implementation tests (threshold: 30%). The new test is a solid behavioral regression guard. The main gap is error/edge-case coverage, which limits the score to 70/100.


📖 Understanding Test Classifications

Design Tests (High Value) verify observable outputs and would catch behavioral regressions.

Implementation Tests (Low Value) verify internal call patterns and break during legitimate refactoring.

References: §25970175071

🧪 Test quality analysis by Test Quality Sentinel · ● 7.8M ·

@github-actions
Copy link
Copy Markdown
Contributor

@copilot please fix and re-run the failing JS Tests shard 3 check.

Generated by 👨‍🍳 PR Sous Chef ·

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments and review comments

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 16, 2026

@copilot review all comments and review comments

Addressed in e7bc146.

  • add_comment now honors mentions.enabled: false (and mentions: false) by forcing an empty allowlist during handler-time re-sanitization.
  • Added a focused safe_output_handler_manager test verifying top-level mentions config is propagated into add_comment handler config.
  • Reverted the unrelated workflow .lock.yml memory-constraint changes from this PR.

@pelikhan pelikhan merged commit 46bc98c into main May 16, 2026
@pelikhan pelikhan deleted the copilot/review-add-comment-sanitization branch May 16, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants