From 6f746e9a4f9adba739a44346962fe404a5c71c22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:19:15 +0000 Subject: [PATCH 1/9] Initial plan From 2d72a73ac4363414f0553ab72c64574dc36df9dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:27:58 +0000 Subject: [PATCH 2/9] feat: add GitHub Guard DIFC filtered items notice to footer Create gateway_difc_filtered.cjs module that reads DIFC_FILTERED events from MCP gateway logs (gateway.jsonl / rpc-messages.jsonl) and generates a GitHub tip alert section for inclusion in workflow footers. Update messages_footer.cjs to call getDifcFilteredEvents() / generateDifcFilteredSection() inside generateFooterWithMessages(), following the same style as the existing firewall blocked domains notice. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/gateway_difc_filtered.cjs | 119 ++++++++ .../setup/js/gateway_difc_filtered.test.cjs | 278 ++++++++++++++++++ actions/setup/js/messages_footer.cjs | 8 + 3 files changed, 405 insertions(+) create mode 100644 actions/setup/js/gateway_difc_filtered.cjs create mode 100644 actions/setup/js/gateway_difc_filtered.test.cjs diff --git a/actions/setup/js/gateway_difc_filtered.cjs b/actions/setup/js/gateway_difc_filtered.cjs new file mode 100644 index 00000000000..750323080d8 --- /dev/null +++ b/actions/setup/js/gateway_difc_filtered.cjs @@ -0,0 +1,119 @@ +// @ts-check +/// + +/** + * Gateway DIFC Filtered Module + * + * This module handles reading MCP gateway logs and extracting DIFC_FILTERED events + * for display in AI-generated footers. + */ + +const fs = require("fs"); + +const GATEWAY_JSONL_PATH = "/tmp/gh-aw/mcp-logs/gateway.jsonl"; +const RPC_MESSAGES_PATH = "/tmp/gh-aw/mcp-logs/rpc-messages.jsonl"; + +/** + * Parses JSONL content and extracts DIFC_FILTERED events + * @param {string} jsonlContent - The JSONL file content + * @returns {Array} Array of DIFC_FILTERED event objects + */ +function parseDifcFilteredEvents(jsonlContent) { + const filteredEvents = []; + const lines = jsonlContent.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || !trimmed.includes("DIFC_FILTERED")) continue; + try { + const entry = JSON.parse(trimmed); + if (entry.type === "DIFC_FILTERED") { + filteredEvents.push(entry); + } + } catch { + // skip malformed lines + } + } + return filteredEvents; +} + +/** + * Reads DIFC_FILTERED events from MCP gateway logs. + * + * This function checks two possible locations for gateway logs: + * 1. Path specified by gatewayJsonlPath (or /tmp/gh-aw/mcp-logs/gateway.jsonl by default) + * 2. Path specified by rpcMessagesPath (or /tmp/gh-aw/mcp-logs/rpc-messages.jsonl as fallback) + * + * @param {string} [gatewayJsonlPath] - Path to gateway.jsonl. Defaults to /tmp/gh-aw/mcp-logs/gateway.jsonl + * @param {string} [rpcMessagesPath] - Path to rpc-messages.jsonl fallback. Defaults to /tmp/gh-aw/mcp-logs/rpc-messages.jsonl + * @returns {Array} Array of DIFC_FILTERED event objects + */ +function getDifcFilteredEvents(gatewayJsonlPath, rpcMessagesPath) { + const jsonlPath = gatewayJsonlPath || GATEWAY_JSONL_PATH; + const rpcPath = rpcMessagesPath || RPC_MESSAGES_PATH; + + if (fs.existsSync(jsonlPath)) { + try { + const content = fs.readFileSync(jsonlPath, "utf8"); + return parseDifcFilteredEvents(content); + } catch { + return []; + } + } + + if (fs.existsSync(rpcPath)) { + try { + const content = fs.readFileSync(rpcPath, "utf8"); + return parseDifcFilteredEvents(content); + } catch { + return []; + } + } + + return []; +} + +/** + * Generates HTML details/summary section for DIFC filtered items wrapped in a GitHub tip alert. + * @param {Array} filteredEvents - Array of DIFC_FILTERED event objects + * @returns {string} GitHub tip alert with details section, or empty string if no filtered events + */ +function generateDifcFilteredSection(filteredEvents) { + if (!filteredEvents || filteredEvents.length === 0) { + return ""; + } + + const count = filteredEvents.length; + const itemWord = count === 1 ? "item" : "items"; + + let section = "\n\n> [!TIP]\n"; + section += `>
\n`; + section += `> 🔒 GitHub Guard filtered ${count} ${itemWord}\n`; + section += `>\n`; + section += `> The GitHub Guard activated and filtered the following ${itemWord} during workflow execution.\n`; + section += `> This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.\n`; + section += `>\n`; + + for (const event of filteredEvents) { + let reference; + if (event.html_url) { + const label = event.number ? `#${event.number}` : event.html_url; + reference = `[${label}](${event.html_url})`; + } else { + reference = event.description || (event.tool_name ? `\`${event.tool_name}\`` : "-"); + } + const tool = event.tool_name ? `\`${event.tool_name}\`` : "-"; + const reason = (event.reason || "-").replace(/\n/g, " "); + section += `> - ${reference} (${tool}: ${reason})\n`; + } + + section += `>\n`; + section += `>
\n`; + + return section; +} + +module.exports = { + parseDifcFilteredEvents, + getDifcFilteredEvents, + generateDifcFilteredSection, +}; diff --git a/actions/setup/js/gateway_difc_filtered.test.cjs b/actions/setup/js/gateway_difc_filtered.test.cjs new file mode 100644 index 00000000000..c4049f80213 --- /dev/null +++ b/actions/setup/js/gateway_difc_filtered.test.cjs @@ -0,0 +1,278 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +describe("gateway_difc_filtered.cjs", () => { + let parseDifcFilteredEvents; + let getDifcFilteredEvents; + let generateDifcFilteredSection; + let testDir; + + beforeEach(async () => { + // Create a temporary directory for test files + testDir = path.join(os.tmpdir(), `gh-aw-test-difc-${Date.now()}`); + fs.mkdirSync(testDir, { recursive: true }); + + // Dynamic import to get fresh module state + const module = await import("./gateway_difc_filtered.cjs"); + parseDifcFilteredEvents = module.parseDifcFilteredEvents; + getDifcFilteredEvents = module.getDifcFilteredEvents; + generateDifcFilteredSection = module.generateDifcFilteredSection; + }); + + afterEach(() => { + // Clean up test directory + if (testDir && fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + }); + + describe("parseDifcFilteredEvents", () => { + it("should return empty array for empty content", () => { + expect(parseDifcFilteredEvents("")).toEqual([]); + expect(parseDifcFilteredEvents("\n\n")).toEqual([]); + }); + + it("should extract DIFC_FILTERED events from JSONL content", () => { + const content = [ + JSON.stringify({ + timestamp: "2026-03-18T17:30:00Z", + type: "DIFC_FILTERED", + server_id: "github", + tool_name: "list_issues", + reason: "Integrity check failed", + html_url: "https://github.com/org/repo/issues/42", + number: "42", + }), + JSON.stringify({ timestamp: "2026-03-18T17:30:01Z", type: "RESPONSE", server_id: "github" }), + ].join("\n"); + + const events = parseDifcFilteredEvents(content); + expect(events).toHaveLength(1); + expect(events[0].tool_name).toBe("list_issues"); + expect(events[0].number).toBe("42"); + }); + + it("should extract multiple DIFC_FILTERED events", () => { + const content = [JSON.stringify({ type: "DIFC_FILTERED", tool_name: "tool1", reason: "r1" }), JSON.stringify({ type: "DIFC_FILTERED", tool_name: "tool2", reason: "r2" }), JSON.stringify({ type: "REQUEST", tool_name: "tool3" })].join( + "\n" + ); + + const events = parseDifcFilteredEvents(content); + expect(events).toHaveLength(2); + expect(events[0].tool_name).toBe("tool1"); + expect(events[1].tool_name).toBe("tool2"); + }); + + it("should skip malformed JSON lines", () => { + const content = ["{ not valid json", JSON.stringify({ type: "DIFC_FILTERED", tool_name: "valid_tool" }), "another bad line"].join("\n"); + + const events = parseDifcFilteredEvents(content); + expect(events).toHaveLength(1); + expect(events[0].tool_name).toBe("valid_tool"); + }); + + it("should skip blank lines", () => { + const content = "\n" + JSON.stringify({ type: "DIFC_FILTERED", tool_name: "t1" }) + "\n\n" + JSON.stringify({ type: "DIFC_FILTERED", tool_name: "t2" }) + "\n"; + + const events = parseDifcFilteredEvents(content); + expect(events).toHaveLength(2); + }); + + it("should ignore lines without DIFC_FILTERED string for efficiency", () => { + const content = [JSON.stringify({ type: "REQUEST", tool_name: "not_filtered" }), JSON.stringify({ type: "RESPONSE", result: "ok" })].join("\n"); + + const events = parseDifcFilteredEvents(content); + expect(events).toHaveLength(0); + }); + }); + + describe("getDifcFilteredEvents", () => { + it("should return empty array when neither log file exists", () => { + const nonExistent1 = path.join(testDir, "nonexistent1.jsonl"); + const nonExistent2 = path.join(testDir, "nonexistent2.jsonl"); + + const events = getDifcFilteredEvents(nonExistent1, nonExistent2); + expect(events).toEqual([]); + }); + + it("should read events from primary gateway.jsonl path", () => { + const jsonlPath = path.join(testDir, "gateway.jsonl"); + const content = JSON.stringify({ type: "DIFC_FILTERED", tool_name: "list_issues", reason: "test" }); + fs.writeFileSync(jsonlPath, content); + + const events = getDifcFilteredEvents(jsonlPath, path.join(testDir, "rpc.jsonl")); + expect(events).toHaveLength(1); + expect(events[0].tool_name).toBe("list_issues"); + }); + + it("should fall back to rpc-messages.jsonl when gateway.jsonl does not exist", () => { + const rpcPath = path.join(testDir, "rpc-messages.jsonl"); + const content = JSON.stringify({ type: "DIFC_FILTERED", tool_name: "get_issue", reason: "secrecy" }); + fs.writeFileSync(rpcPath, content); + + const events = getDifcFilteredEvents(path.join(testDir, "nonexistent.jsonl"), rpcPath); + expect(events).toHaveLength(1); + expect(events[0].tool_name).toBe("get_issue"); + }); + + it("should prefer primary path over fallback when both exist", () => { + const jsonlPath = path.join(testDir, "gateway.jsonl"); + const rpcPath = path.join(testDir, "rpc-messages.jsonl"); + fs.writeFileSync(jsonlPath, JSON.stringify({ type: "DIFC_FILTERED", tool_name: "primary_tool" })); + fs.writeFileSync(rpcPath, JSON.stringify({ type: "DIFC_FILTERED", tool_name: "fallback_tool" })); + + const events = getDifcFilteredEvents(jsonlPath, rpcPath); + expect(events).toHaveLength(1); + expect(events[0].tool_name).toBe("primary_tool"); + }); + + it("should return empty array when log file is empty", () => { + const jsonlPath = path.join(testDir, "gateway.jsonl"); + fs.writeFileSync(jsonlPath, ""); + + const events = getDifcFilteredEvents(jsonlPath, path.join(testDir, "rpc.jsonl")); + expect(events).toEqual([]); + }); + }); + + describe("generateDifcFilteredSection", () => { + it("should return empty string when no filtered events", () => { + expect(generateDifcFilteredSection([])).toBe(""); + expect(generateDifcFilteredSection(null)).toBe(""); + expect(generateDifcFilteredSection(undefined)).toBe(""); + }); + + it("should generate tip alert section for single filtered item", () => { + const events = [ + { + type: "DIFC_FILTERED", + tool_name: "list_issues", + reason: "Integrity check failed", + html_url: "https://github.com/org/repo/issues/42", + number: "42", + }, + ]; + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("> [!TIP]"); + expect(result).toContain(">
"); + expect(result).toContain(">
"); + expect(result).toContain("> 🔒 GitHub Guard filtered 1 item"); + expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); + expect(result).toContain("`list_issues`"); + expect(result).toContain("Integrity check failed"); + }); + + it("should generate tip alert section for multiple filtered items", () => { + const events = [ + { + type: "DIFC_FILTERED", + tool_name: "list_issues", + reason: "Integrity check failed", + html_url: "https://github.com/org/repo/issues/42", + number: "42", + }, + { + type: "DIFC_FILTERED", + tool_name: "get_issue", + reason: "Secrecy check failed", + html_url: "https://github.com/org/repo/issues/99", + number: "99", + }, + ]; + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("> [!TIP]"); + expect(result).toContain("> 🔒 GitHub Guard filtered 2 items"); + expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); + expect(result).toContain("[#99](https://github.com/org/repo/issues/99)"); + }); + + it("should use description as reference when html_url is absent", () => { + const events = [ + { + type: "DIFC_FILTERED", + tool_name: "list_issues", + description: "resource:list_issues", + reason: "Integrity check failed", + }, + ]; + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("resource:list_issues"); + expect(result).not.toContain("[#"); + }); + + it("should use tool_name as reference when html_url and description are absent", () => { + const events = [ + { + type: "DIFC_FILTERED", + tool_name: "my_tool", + reason: "check failed", + }, + ]; + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("`my_tool`"); + }); + + it("should use html_url directly as label when number is absent", () => { + const events = [ + { + type: "DIFC_FILTERED", + tool_name: "list_repos", + reason: "Integrity check failed", + html_url: "https://github.com/org/repo", + }, + ]; + + const result = generateDifcFilteredSection(events); + + // html_url used as label when no number + expect(result).toContain("[https://github.com/org/repo](https://github.com/org/repo)"); + }); + + it("should include explanation text about why filtering happened", () => { + const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "reason" }]; + const result = generateDifcFilteredSection(events); + + expect(result).toContain("GitHub Guard activated"); + expect(result).toContain("integrity or secrecy level"); + }); + + it("should start with double newline and tip alert", () => { + const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "reason" }]; + const result = generateDifcFilteredSection(events); + + expect(result).toMatch(/^\n\n> \[!TIP\]/); + }); + + it("should use correct singular/plural form", () => { + const singleEvent = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "reason" }]; + const singleResult = generateDifcFilteredSection(singleEvent); + expect(singleResult).toContain("1 item"); + expect(singleResult).not.toContain("items"); + + const multiEvents = [ + { type: "DIFC_FILTERED", tool_name: "tool1", reason: "r1" }, + { type: "DIFC_FILTERED", tool_name: "tool2", reason: "r2" }, + ]; + const multiResult = generateDifcFilteredSection(multiEvents); + expect(multiResult).toContain("2 items"); + }); + + it("should replace newlines in reason with spaces", () => { + const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "line1\nline2" }]; + const result = generateDifcFilteredSection(events); + + expect(result).toContain("line1 line2"); + expect(result).not.toContain("line1\nline2"); + }); + }); +}); diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index 442878c348a..9c951cb1b29 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -11,6 +11,7 @@ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); const { getMissingInfoSections } = require("./missing_messages_helper.cjs"); const { getBlockedDomains, generateBlockedDomainsSection } = require("./firewall_blocked_domains.cjs"); +const { getDifcFilteredEvents, generateDifcFilteredSection } = require("./gateway_difc_filtered.cjs"); /** * @typedef {Object} FooterContext @@ -301,6 +302,13 @@ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workfl footer += blockedDomainsSection; } + // Add GitHub Guard DIFC filtered section if any items were filtered + const difcFilteredEvents = getDifcFilteredEvents(); + const difcFilteredSection = generateDifcFilteredSection(difcFilteredEvents); + if (difcFilteredSection) { + footer += difcFilteredSection; + } + // Add XML comment marker for traceability footer += "\n\n" + generateXMLMarker(workflowName, runUrl); From 0e98394b8a7c05c1bd19cfedf87f1e5c6b16b77b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:53:09 +0000 Subject: [PATCH 3/9] fix: add gateway_difc_filtered.cjs to SAFE_OUTPUTS_FILES in setup.sh Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 6c943eda634..94f5aedecac 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -266,6 +266,7 @@ SAFE_OUTPUTS_FILES=( "handler_auth.cjs" "missing_messages_helper.cjs" "firewall_blocked_domains.cjs" + "gateway_difc_filtered.cjs" "missing_info_formatter.cjs" "sanitize_content_core.cjs" "markdown_code_region_balancer.cjs" From e759a1ce2fdcb862d774ec93b213c59ba8ee9e79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:21:35 +0000 Subject: [PATCH 4/9] Add changeset [skip-ci] --- .changeset/patch-github-guard-difc-footer-notice.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-github-guard-difc-footer-notice.md diff --git a/.changeset/patch-github-guard-difc-footer-notice.md b/.changeset/patch-github-guard-difc-footer-notice.md new file mode 100644 index 00000000000..8b0cc9e2ee4 --- /dev/null +++ b/.changeset/patch-github-guard-difc-footer-notice.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add a footer tip notice when GitHub Guard filters DIFC items during workflow execution, including linked GitHub references and filter reasons. From 29edd01cfc6297dccaafa8e09e2c2c4f79cbc9da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:29:45 +0000 Subject: [PATCH 5/9] fix: deduplicate DIFC filtered events and rename TIP to NOTE Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/gateway_difc_filtered.cjs | 15 ++++++++++--- .../setup/js/gateway_difc_filtered.test.cjs | 22 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/gateway_difc_filtered.cjs b/actions/setup/js/gateway_difc_filtered.cjs index 750323080d8..f0c5acd7fa8 100644 --- a/actions/setup/js/gateway_difc_filtered.cjs +++ b/actions/setup/js/gateway_difc_filtered.cjs @@ -82,10 +82,19 @@ function generateDifcFilteredSection(filteredEvents) { return ""; } - const count = filteredEvents.length; + // Deduplicate events by their significant fields + const seen = new Set(); + const uniqueEvents = filteredEvents.filter(event => { + const key = [event.html_url || "", event.tool_name || "", event.description || "", event.reason || ""].join("|"); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + const count = uniqueEvents.length; const itemWord = count === 1 ? "item" : "items"; - let section = "\n\n> [!TIP]\n"; + let section = "\n\n> [!NOTE]\n"; section += `>
\n`; section += `> 🔒 GitHub Guard filtered ${count} ${itemWord}\n`; section += `>\n`; @@ -93,7 +102,7 @@ function generateDifcFilteredSection(filteredEvents) { section += `> This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.\n`; section += `>\n`; - for (const event of filteredEvents) { + for (const event of uniqueEvents) { let reference; if (event.html_url) { const label = event.number ? `#${event.number}` : event.html_url; diff --git a/actions/setup/js/gateway_difc_filtered.test.cjs b/actions/setup/js/gateway_difc_filtered.test.cjs index c4049f80213..4d41197dfa2 100644 --- a/actions/setup/js/gateway_difc_filtered.test.cjs +++ b/actions/setup/js/gateway_difc_filtered.test.cjs @@ -157,7 +157,7 @@ describe("gateway_difc_filtered.cjs", () => { const result = generateDifcFilteredSection(events); - expect(result).toContain("> [!TIP]"); + expect(result).toContain("> [!NOTE]"); expect(result).toContain(">
"); expect(result).toContain(">
"); expect(result).toContain("> 🔒 GitHub Guard filtered 1 item"); @@ -186,7 +186,7 @@ describe("gateway_difc_filtered.cjs", () => { const result = generateDifcFilteredSection(events); - expect(result).toContain("> [!TIP]"); + expect(result).toContain("> [!NOTE]"); expect(result).toContain("> 🔒 GitHub Guard filtered 2 items"); expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); expect(result).toContain("[#99](https://github.com/org/repo/issues/99)"); @@ -246,11 +246,11 @@ describe("gateway_difc_filtered.cjs", () => { expect(result).toContain("integrity or secrecy level"); }); - it("should start with double newline and tip alert", () => { + it("should start with double newline and note alert", () => { const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "reason" }]; const result = generateDifcFilteredSection(events); - expect(result).toMatch(/^\n\n> \[!TIP\]/); + expect(result).toMatch(/^\n\n> \[!NOTE\]/); }); it("should use correct singular/plural form", () => { @@ -267,6 +267,20 @@ describe("gateway_difc_filtered.cjs", () => { expect(multiResult).toContain("2 items"); }); + it("should deduplicate filtered events with identical fields", () => { + const events = [ + { type: "DIFC_FILTERED", tool_name: "list_issues", reason: "Integrity check failed", html_url: "https://github.com/org/repo/issues/42", number: "42" }, + { type: "DIFC_FILTERED", tool_name: "list_issues", reason: "Integrity check failed", html_url: "https://github.com/org/repo/issues/42", number: "42" }, + { type: "DIFC_FILTERED", tool_name: "get_issue", reason: "Secrecy check failed", html_url: "https://github.com/org/repo/issues/99", number: "99" }, + ]; + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("> 🔒 GitHub Guard filtered 2 items"); + expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); + expect(result).toContain("[#99](https://github.com/org/repo/issues/99)"); + }); + it("should replace newlines in reason with spaces", () => { const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "line1\nline2" }]; const result = generateDifcFilteredSection(events); From ea600f44d6acd1142b11df05d17f8f57e6692a86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:52:22 +0000 Subject: [PATCH 6/9] Wrap DIFC section in try/catch to preserve rest of footer on error Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/messages_footer.cjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index 9c951cb1b29..c80670e138e 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -303,10 +303,14 @@ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workfl } // Add GitHub Guard DIFC filtered section if any items were filtered - const difcFilteredEvents = getDifcFilteredEvents(); - const difcFilteredSection = generateDifcFilteredSection(difcFilteredEvents); - if (difcFilteredSection) { - footer += difcFilteredSection; + try { + const difcFilteredEvents = getDifcFilteredEvents(); + const difcFilteredSection = generateDifcFilteredSection(difcFilteredEvents); + if (difcFilteredSection) { + footer += difcFilteredSection; + } + } catch { + // ignore errors so the rest of the footer is always preserved } // Add XML comment marker for traceability From c7481a3a35bbbbd92b20f2a7301ff4facfc466f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:40:20 +0000 Subject: [PATCH 7/9] Move guard notices (firewall + DIFC) before attribution footer in generateFooterWithMessages Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/messages_footer.cjs | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index c80670e138e..48519071e7e 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -281,25 +281,14 @@ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workfl historyUrl: historyUrl || undefined, }; - let footer = "\n\n" + getFooterMessage(ctx); - - // Add installation instructions if source is available - const installMessage = getFooterInstallMessage(ctx); - if (installMessage) { - footer += "\n>\n" + installMessage; - } - - // Add missing tools and data sections if available - const missingInfoSections = getMissingInfoSections(); - if (missingInfoSections) { - footer += missingInfoSections; - } + // Collect guard notices to show BEFORE the attribution footer + let guardNotices = ""; // Add firewall blocked domains section if any domains were blocked const blockedDomains = getBlockedDomains(); const blockedDomainsSection = generateBlockedDomainsSection(blockedDomains); if (blockedDomainsSection) { - footer += blockedDomainsSection; + guardNotices += blockedDomainsSection; } // Add GitHub Guard DIFC filtered section if any items were filtered @@ -307,12 +296,27 @@ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workfl const difcFilteredEvents = getDifcFilteredEvents(); const difcFilteredSection = generateDifcFilteredSection(difcFilteredEvents); if (difcFilteredSection) { - footer += difcFilteredSection; + guardNotices += difcFilteredSection; } } catch { // ignore errors so the rest of the footer is always preserved } + // Attribution footer line comes after any guard notices + let footer = guardNotices + "\n\n" + getFooterMessage(ctx); + + // Add installation instructions if source is available + const installMessage = getFooterInstallMessage(ctx); + if (installMessage) { + footer += "\n>\n" + installMessage; + } + + // Add missing tools and data sections if available + const missingInfoSections = getMissingInfoSections(); + if (missingInfoSections) { + footer += missingInfoSections; + } + // Add XML comment marker for traceability footer += "\n\n" + generateXMLMarker(workflowName, runUrl); From bfe900e0aa157cf625291e52799775237709fc41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:50:51 +0000 Subject: [PATCH 8/9] Rename 'GitHub Guard DIFC' to 'Integrity filtering' in user-facing text and tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/gateway_difc_filtered.cjs | 8 ++++---- actions/setup/js/gateway_difc_filtered.test.cjs | 8 ++++---- actions/setup/js/messages_footer.cjs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/gateway_difc_filtered.cjs b/actions/setup/js/gateway_difc_filtered.cjs index f0c5acd7fa8..8803e63d111 100644 --- a/actions/setup/js/gateway_difc_filtered.cjs +++ b/actions/setup/js/gateway_difc_filtered.cjs @@ -73,9 +73,9 @@ function getDifcFilteredEvents(gatewayJsonlPath, rpcMessagesPath) { } /** - * Generates HTML details/summary section for DIFC filtered items wrapped in a GitHub tip alert. + * Generates HTML details/summary section for integrity-filtered items wrapped in a GitHub note alert. * @param {Array} filteredEvents - Array of DIFC_FILTERED event objects - * @returns {string} GitHub tip alert with details section, or empty string if no filtered events + * @returns {string} GitHub note alert with details section, or empty string if no filtered events */ function generateDifcFilteredSection(filteredEvents) { if (!filteredEvents || filteredEvents.length === 0) { @@ -96,9 +96,9 @@ function generateDifcFilteredSection(filteredEvents) { let section = "\n\n> [!NOTE]\n"; section += `>
\n`; - section += `> 🔒 GitHub Guard filtered ${count} ${itemWord}\n`; + section += `> 🔒 Integrity filtering filtered ${count} ${itemWord}\n`; section += `>\n`; - section += `> The GitHub Guard activated and filtered the following ${itemWord} during workflow execution.\n`; + section += `> Integrity filtering activated and filtered the following ${itemWord} during workflow execution.\n`; section += `> This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.\n`; section += `>\n`; diff --git a/actions/setup/js/gateway_difc_filtered.test.cjs b/actions/setup/js/gateway_difc_filtered.test.cjs index 4d41197dfa2..0807b6e65df 100644 --- a/actions/setup/js/gateway_difc_filtered.test.cjs +++ b/actions/setup/js/gateway_difc_filtered.test.cjs @@ -160,7 +160,7 @@ describe("gateway_difc_filtered.cjs", () => { expect(result).toContain("> [!NOTE]"); expect(result).toContain(">
"); expect(result).toContain(">
"); - expect(result).toContain("> 🔒 GitHub Guard filtered 1 item"); + expect(result).toContain("> 🔒 Integrity filtering filtered 1 item"); expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); expect(result).toContain("`list_issues`"); expect(result).toContain("Integrity check failed"); @@ -187,7 +187,7 @@ describe("gateway_difc_filtered.cjs", () => { const result = generateDifcFilteredSection(events); expect(result).toContain("> [!NOTE]"); - expect(result).toContain("> 🔒 GitHub Guard filtered 2 items"); + expect(result).toContain("> 🔒 Integrity filtering filtered 2 items"); expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); expect(result).toContain("[#99](https://github.com/org/repo/issues/99)"); }); @@ -242,7 +242,7 @@ describe("gateway_difc_filtered.cjs", () => { const events = [{ type: "DIFC_FILTERED", tool_name: "tool", reason: "reason" }]; const result = generateDifcFilteredSection(events); - expect(result).toContain("GitHub Guard activated"); + expect(result).toContain("Integrity filtering activated"); expect(result).toContain("integrity or secrecy level"); }); @@ -276,7 +276,7 @@ describe("gateway_difc_filtered.cjs", () => { const result = generateDifcFilteredSection(events); - expect(result).toContain("> 🔒 GitHub Guard filtered 2 items"); + expect(result).toContain("> 🔒 Integrity filtering filtered 2 items"); expect(result).toContain("[#42](https://github.com/org/repo/issues/42)"); expect(result).toContain("[#99](https://github.com/org/repo/issues/99)"); }); diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index 48519071e7e..2e41960db16 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -291,7 +291,7 @@ function generateFooterWithMessages(workflowName, runUrl, workflowSource, workfl guardNotices += blockedDomainsSection; } - // Add GitHub Guard DIFC filtered section if any items were filtered + // Add integrity filtering section if any items were filtered try { const difcFilteredEvents = getDifcFilteredEvents(); const difcFilteredSection = generateDifcFilteredSection(difcFilteredEvents); From 1a17129535279dee840b60629b075892709eca4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:02:05 +0000 Subject: [PATCH 9/9] Limit displayed integrity filtering items to 16 with ellipsis for the rest Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/gateway_difc_filtered.cjs | 10 +++- .../setup/js/gateway_difc_filtered.test.cjs | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/gateway_difc_filtered.cjs b/actions/setup/js/gateway_difc_filtered.cjs index 8803e63d111..a55f5713763 100644 --- a/actions/setup/js/gateway_difc_filtered.cjs +++ b/actions/setup/js/gateway_difc_filtered.cjs @@ -102,7 +102,11 @@ function generateDifcFilteredSection(filteredEvents) { section += `> This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.\n`; section += `>\n`; - for (const event of uniqueEvents) { + const maxItems = 16; + const visibleEvents = uniqueEvents.slice(0, maxItems); + const remainingCount = uniqueEvents.length - visibleEvents.length; + + for (const event of visibleEvents) { let reference; if (event.html_url) { const label = event.number ? `#${event.number}` : event.html_url; @@ -115,6 +119,10 @@ function generateDifcFilteredSection(filteredEvents) { section += `> - ${reference} (${tool}: ${reason})\n`; } + if (remainingCount > 0) { + section += `> - ... and ${remainingCount} more ${remainingCount === 1 ? "item" : "items"}\n`; + } + section += `>\n`; section += `>
\n`; diff --git a/actions/setup/js/gateway_difc_filtered.test.cjs b/actions/setup/js/gateway_difc_filtered.test.cjs index 0807b6e65df..1b36430eaad 100644 --- a/actions/setup/js/gateway_difc_filtered.test.cjs +++ b/actions/setup/js/gateway_difc_filtered.test.cjs @@ -288,5 +288,52 @@ describe("gateway_difc_filtered.cjs", () => { expect(result).toContain("line1 line2"); expect(result).not.toContain("line1\nline2"); }); + + it("should show at most 16 items and ellipse the rest", () => { + const events = Array.from({ length: 20 }, (_, i) => ({ + type: "DIFC_FILTERED", + tool_name: `tool_${i}`, + reason: "reason", + html_url: `https://github.com/org/repo/issues/${i + 1}`, + number: String(i + 1), + })); + + const result = generateDifcFilteredSection(events); + + // Summary still shows the total count + expect(result).toContain("> 🔒 Integrity filtering filtered 20 items"); + // First 16 items rendered + expect(result).toContain("[#1](https://github.com/org/repo/issues/1)"); + expect(result).toContain("[#16](https://github.com/org/repo/issues/16)"); + // Items 17-20 not rendered individually + expect(result).not.toContain("[#17]"); + // Ellipsis line present + expect(result).toContain("... and 4 more items"); + }); + + it("should not show ellipsis when 16 or fewer items", () => { + const events = Array.from({ length: 16 }, (_, i) => ({ + type: "DIFC_FILTERED", + tool_name: `tool_${i}`, + reason: "reason", + })); + + const result = generateDifcFilteredSection(events); + + expect(result).not.toContain("more item"); + }); + + it("should use singular form in ellipsis for exactly 1 remaining item", () => { + const events = Array.from({ length: 17 }, (_, i) => ({ + type: "DIFC_FILTERED", + tool_name: `tool_${i}`, + reason: "reason", + })); + + const result = generateDifcFilteredSection(events); + + expect(result).toContain("... and 1 more item"); + expect(result).not.toContain("... and 1 more items"); + }); }); });