From 5bcc55ca9676690cd3ca5400915f92fcf50093ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:20:39 +0000 Subject: [PATCH 1/7] feat: render RPC message types in gateway summary Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 223 +++++++++++++++--- .../setup/js/parse_mcp_gateway_log.test.cjs | 32 ++- 2 files changed, 220 insertions(+), 35 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index a4b65537c4f..3e4858f43b9 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -320,6 +320,131 @@ function getRpcRequestLabel(entry) { return method || "unknown"; } +/** + * Formats an rpc-messages timestamp for display in the step summary. + * @param {string|undefined} timestamp + * @returns {string} + */ +function formatRpcMessageTime(timestamp) { + return timestamp ? timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z") : "-"; +} + +/** + * Escapes text for safe display inside a markdown table cell. + * @param {unknown} value + * @returns {string} + */ +function escapeMarkdownTableCell(value) { + return String(value ?? "-") + .replace(/\n/g, " ") + .replace(/\|/g, "\\|") + .trim(); +} + +/** + * Truncates a string to a maximum length, appending an ellipsis when needed. + * @param {string} value + * @param {number} maxLength + * @returns {string} + */ +function truncateSummaryValue(value, maxLength) { + if (value.length <= maxLength) return value; + return `${value.slice(0, maxLength - 1)}…`; +} + +/** + * Summarizes an MCP RESPONSE entry for table rendering. + * @param {Object} entry + * @returns {{status: string, details: string}} + */ +function summarizeRpcResponseEntry(entry) { + const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : {}; + const error = payload.error && typeof payload.error === "object" ? payload.error : null; + if (error) { + const code = error.code != null ? ` ${error.code}` : ""; + const message = truncateSummaryValue(String(error.message || "Unknown error"), 120); + return { + status: "error", + details: `error${code}: ${message}`, + }; + } + + const result = payload.result; + if (result && typeof result === "object") { + if (Array.isArray(result.tools)) { + return { + status: "ok", + details: `${result.tools.length} tool${result.tools.length !== 1 ? "s" : ""}`, + }; + } + + const keys = Object.keys(result); + if (keys.length > 0) { + const shownKeys = keys.slice(0, 3); + const moreCount = keys.length - shownKeys.length; + return { + status: "ok", + details: `result keys: ${shownKeys.join(", ")}${moreCount > 0 ? ` +${moreCount} more` : ""}`, + }; + } + } + + if (result !== undefined) { + return { + status: "ok", + details: truncateSummaryValue(JSON.stringify(result), 120), + }; + } + + return { + status: "ok", + details: "response received", + }; +} + +/** + * Summarizes a non-REQUEST rpc-messages entry for table rendering. + * @param {Object} entry + * @returns {string} + */ +function summarizeGenericRpcEntry(entry) { + const parts = []; + const topLevelIgnoredKeys = new Set(["timestamp", "direction", "type", "server_id", "payload"]); + + for (const [key, value] of Object.entries(entry)) { + if (topLevelIgnoredKeys.has(key) || value == null || typeof value === "object") continue; + parts.push(`${key}=${value}`); + } + + const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : null; + if (payload) { + if (payload.method) { + parts.push(`method=${payload.method}`); + } + if (payload.params && typeof payload.params === "object" && payload.params.name) { + parts.push(`tool=${payload.params.name}`); + } + if (payload.id != null) { + parts.push(`id=${payload.id}`); + } + if (payload.error && typeof payload.error === "object" && payload.error.message) { + parts.push(`error=${payload.error.message}`); + } + if (parts.length === 0) { + const payloadKeys = Object.keys(payload); + if (payloadKeys.length > 0) { + parts.push(`payload keys=${payloadKeys.join(", ")}`); + } + } + } + + if (parts.length === 0) { + return "—"; + } + + return truncateSummaryValue(parts.join(" · "), 160); +} + /** * Generates a markdown step summary for rpc-messages.jsonl entries (mcpg v0.2.0+ format). * Shows a table of REQUEST entries (tool calls), a count of RESPONSE entries, any other @@ -336,41 +461,85 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { if (totalMessages === 0) return ""; const parts = []; + /** @type {Record>} */ + const otherByType = {}; + for (const entry of other) { + otherByType[entry.type] ??= []; + otherByType[entry.type].push(entry); + } + const renderedOtherTypes = Object.keys(otherByType); + + if (requests.length === 0 && responses.length === 0 && other.length === 0 && blockedCount > 0) { + // No requests, but there are DIFC_FILTERED events — add a minimal header + parts.push(`
\nMCP Gateway Activity (${blockedCount} blocked)\n\n*All tool calls were blocked by the integrity filter.*\n\n
\n`); + } else { + const summaryParts = []; + if (requests.length > 0) { + summaryParts.push(`${requests.length} request${requests.length !== 1 ? "s" : ""}`); + } + if (responses.length > 0) { + summaryParts.push(`${responses.length} response${responses.length !== 1 ? "s" : ""}`); + } + for (const type of renderedOtherTypes) { + const count = otherByType[type].length; + summaryParts.push(`${count} ${type}`); + } + if (blockedCount > 0) { + summaryParts.push(`${blockedCount} blocked`); + } - // Tool calls / requests table - if (requests.length > 0) { - const blockedNote = blockedCount > 0 ? `, ${blockedCount} blocked` : ""; const callLines = []; callLines.push("
"); - callLines.push(`MCP Gateway Activity (${requests.length} request${requests.length !== 1 ? "s" : ""}${blockedNote})\n`); + callLines.push(`MCP Gateway Activity (${summaryParts.join(", ")})\n`); callLines.push(""); - callLines.push("| Time | Server | Tool / Method |"); - callLines.push("|------|--------|---------------|"); - - for (const req of requests) { - const time = req.timestamp ? req.timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z") : "-"; - const server = req.server_id || "-"; - const label = getRpcRequestLabel(req); - callLines.push(`| ${time} | ${server} | \`${label}\` |`); + + if (requests.length > 0) { + callLines.push("#### REQUEST"); + callLines.push(""); + callLines.push("| Time | Server | Tool / Method |"); + callLines.push("|------|--------|---------------|"); + + for (const req of requests) { + const time = formatRpcMessageTime(req.timestamp); + const server = escapeMarkdownTableCell(req.server_id || "-"); + const label = escapeMarkdownTableCell(getRpcRequestLabel(req)); + callLines.push(`| ${time} | ${server} | \`${label}\` |`); + } + + callLines.push(""); } - callLines.push(""); - callLines.push("
\n"); - parts.push(callLines.join("\n")); - } else if (blockedCount > 0) { - // No requests, but there are DIFC_FILTERED events — add a minimal header - parts.push(`
\nMCP Gateway Activity (${blockedCount} blocked)\n\n*All tool calls were blocked by the integrity filter.*\n\n
\n`); - } + if (responses.length > 0) { + callLines.push("#### RESPONSE"); + callLines.push(""); + callLines.push("| Time | Server | Direction | Status | Details |"); + callLines.push("|------|--------|-----------|--------|---------|"); + + for (const response of responses) { + const { status, details } = summarizeRpcResponseEntry(response); + callLines.push( + `| ${formatRpcMessageTime(response.timestamp)} | ${escapeMarkdownTableCell(response.server_id || "-")} | ${escapeMarkdownTableCell(response.direction || "-")} | ${escapeMarkdownTableCell(status)} | ${escapeMarkdownTableCell(details)} |` + ); + } - // Other message types (not REQUEST, RESPONSE, DIFC_FILTERED) - if (other.length > 0) { - /** @type {Record} */ - const typeCounts = {}; - for (const entry of other) { - typeCounts[entry.type] = (typeCounts[entry.type] || 0) + 1; + callLines.push(""); } - const otherLines = Object.entries(typeCounts).map(([type, count]) => `- **${type}**: ${count} message${count !== 1 ? "s" : ""}`); - parts.push("
\nOther Gateway Messages\n\n" + otherLines.join("\n") + "\n\n
\n"); + + for (const type of renderedOtherTypes) { + callLines.push(`#### ${type}`); + callLines.push(""); + callLines.push("| Time | Server | Direction | Details |"); + callLines.push("|------|--------|-----------|---------|"); + + for (const entry of otherByType[type]) { + callLines.push(`| ${formatRpcMessageTime(entry.timestamp)} | ${escapeMarkdownTableCell(entry.server_id || "-")} | ${escapeMarkdownTableCell(entry.direction || "-")} | ${escapeMarkdownTableCell(summarizeGenericRpcEntry(entry))} |`); + } + + callLines.push(""); + } + + callLines.push("\n"); + parts.push(callLines.join("\n")); } // DIFC_FILTERED section (re-uses existing table renderer) diff --git a/actions/setup/js/parse_mcp_gateway_log.test.cjs b/actions/setup/js/parse_mcp_gateway_log.test.cjs index 8b78b176f87..8cd3e615899 100644 --- a/actions/setup/js/parse_mcp_gateway_log.test.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.test.cjs @@ -921,12 +921,13 @@ Some content here.`; test("generates details/summary with request count", () => { const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: sampleResponses, other: [] }, []); expect(summary).toContain("
"); - expect(summary).toContain("MCP Gateway Activity (2 requests)"); + expect(summary).toContain("MCP Gateway Activity (2 requests, 1 response)"); expect(summary).toContain("
"); }); test("renders request table with time, server, and tool columns", () => { const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: [], other: [] }, []); + expect(summary).toContain("#### REQUEST"); expect(summary).toContain("| Time | Server | Tool / Method |"); expect(summary).toContain("`list_issues`"); expect(summary).toContain("`add_comment`"); @@ -945,6 +946,20 @@ Some content here.`; expect(summary).toContain("1 blocked"); }); + test("renders response rows with status and details", () => { + const responses = [ + { timestamp: "2026-01-18T11:10:50Z", direction: "IN", type: "RESPONSE", server_id: "github", payload: { jsonrpc: "2.0", id: 1, result: { tools: [{ name: "list_issues" }] } } }, + { timestamp: "2026-01-18T11:10:52Z", direction: "IN", type: "RESPONSE", server_id: "safeoutputs", payload: { jsonrpc: "2.0", id: 2, error: { code: -32603, message: "Tool failed" } } }, + ]; + + const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses, other: [] }, []); + expect(summary).toContain("#### RESPONSE"); + expect(summary).toContain("| Time | Server | Direction | Status | Details |"); + expect(summary).toContain("1 tool"); + expect(summary).toContain("error"); + expect(summary).toContain("Tool failed"); + }); + test("includes DIFC_FILTERED table when events are present", () => { const difcEvents = [{ type: "DIFC_FILTERED", tool_name: "get_issue", server_id: "github", reason: "Integrity check failed", author_login: "user1", author_association: "MEMBER" }]; const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: [], other: [] }, difcEvents); @@ -954,15 +969,16 @@ Some content here.`; test("renders other message types section", () => { const other = [ - { type: "SESSION_START", server_id: "github" }, - { type: "SESSION_START", server_id: "github" }, - { type: "SESSION_END", server_id: "github" }, + { timestamp: "2026-01-18T11:10:40Z", direction: "OUT", type: "SESSION_START", server_id: "github", session_id: "abc123" }, + { timestamp: "2026-01-18T11:10:41Z", direction: "OUT", type: "SESSION_START", server_id: "github", session_id: "def456" }, + { timestamp: "2026-01-18T11:10:55Z", direction: "IN", type: "SESSION_END", server_id: "github", reason: "completed" }, ]; const summary = generateRpcMessagesSummary({ requests: [], responses: [], other }, []); - expect(summary).toContain("Other Gateway Messages"); - expect(summary).toContain("SESSION_START"); - expect(summary).toContain("SESSION_END"); - expect(summary).toContain("2 messages"); + expect(summary).toContain("MCP Gateway Activity (2 SESSION_START, 1 SESSION_END)"); + expect(summary).toContain("#### SESSION_START"); + expect(summary).toContain("#### SESSION_END"); + expect(summary).toContain("session_id=abc123"); + expect(summary).toContain("reason=completed"); }); test("shows minimal header when only DIFC events exist (no requests)", () => { From 0f19f25349c0336cbb413103968a6dac27f523de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:23:47 +0000 Subject: [PATCH 2/7] chore: address gateway summary review feedback Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 3e4858f43b9..5904dd99ed8 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -18,6 +18,8 @@ const { computeEffectiveTokens, getTokenClassWeights, formatET } = require("./ef */ const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl"; +const MAX_RPC_SUMMARY_DETAILS_LENGTH = 120; +const MAX_RPC_SUMMARY_GENERIC_LENGTH = 160; /** * Formats milliseconds as a human-readable duration string. @@ -349,7 +351,7 @@ function escapeMarkdownTableCell(value) { */ function truncateSummaryValue(value, maxLength) { if (value.length <= maxLength) return value; - return `${value.slice(0, maxLength - 1)}…`; + return `${value.slice(0, maxLength - 3)}...`; } /** @@ -362,7 +364,7 @@ function summarizeRpcResponseEntry(entry) { const error = payload.error && typeof payload.error === "object" ? payload.error : null; if (error) { const code = error.code != null ? ` ${error.code}` : ""; - const message = truncateSummaryValue(String(error.message || "Unknown error"), 120); + const message = truncateSummaryValue(String(error.message || "Unknown error"), MAX_RPC_SUMMARY_DETAILS_LENGTH); return { status: "error", details: `error${code}: ${message}`, @@ -392,7 +394,7 @@ function summarizeRpcResponseEntry(entry) { if (result !== undefined) { return { status: "ok", - details: truncateSummaryValue(JSON.stringify(result), 120), + details: truncateSummaryValue(JSON.stringify(result), MAX_RPC_SUMMARY_DETAILS_LENGTH), }; } @@ -439,10 +441,10 @@ function summarizeGenericRpcEntry(entry) { } if (parts.length === 0) { - return "—"; + return "-"; } - return truncateSummaryValue(parts.join(" · "), 160); + return truncateSummaryValue(parts.join(" · "), MAX_RPC_SUMMARY_GENERIC_LENGTH); } /** From 0cafde6baaa0fb1aa2112782690de7b6f603c2e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:26:14 +0000 Subject: [PATCH 3/7] chore: harden gateway summary rendering Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 34 +++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 5904dd99ed8..03719b15c3a 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -351,6 +351,7 @@ function escapeMarkdownTableCell(value) { */ function truncateSummaryValue(value, maxLength) { if (value.length <= maxLength) return value; + if (maxLength < 3) return value.slice(0, Math.max(maxLength, 0)); return `${value.slice(0, maxLength - 3)}...`; } @@ -412,30 +413,33 @@ function summarizeRpcResponseEntry(entry) { function summarizeGenericRpcEntry(entry) { const parts = []; const topLevelIgnoredKeys = new Set(["timestamp", "direction", "type", "server_id", "payload"]); + const pushPart = (key, value) => { + parts.push(`${key}=${truncateSummaryValue(String(value), MAX_RPC_SUMMARY_DETAILS_LENGTH)}`); + }; for (const [key, value] of Object.entries(entry)) { if (topLevelIgnoredKeys.has(key) || value == null || typeof value === "object") continue; - parts.push(`${key}=${value}`); + pushPart(key, value); } const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : null; if (payload) { if (payload.method) { - parts.push(`method=${payload.method}`); + pushPart("method", payload.method); } if (payload.params && typeof payload.params === "object" && payload.params.name) { - parts.push(`tool=${payload.params.name}`); + pushPart("tool", payload.params.name); } if (payload.id != null) { - parts.push(`id=${payload.id}`); + pushPart("id", payload.id); } if (payload.error && typeof payload.error === "object" && payload.error.message) { - parts.push(`error=${payload.error.message}`); + pushPart("error", payload.error.message); } if (parts.length === 0) { const payloadKeys = Object.keys(payload); if (payloadKeys.length > 0) { - parts.push(`payload keys=${payloadKeys.join(", ")}`); + pushPart("payload keys", payloadKeys.join(", ")); } } } @@ -463,13 +467,17 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { if (totalMessages === 0) return ""; const parts = []; - /** @type {Record>} */ - const otherByType = {}; + /** @type {Map>} */ + const otherByType = new Map(); for (const entry of other) { - otherByType[entry.type] ??= []; - otherByType[entry.type].push(entry); + const existingEntries = otherByType.get(entry.type); + if (existingEntries) { + existingEntries.push(entry); + } else { + otherByType.set(entry.type, [entry]); + } } - const renderedOtherTypes = Object.keys(otherByType); + const renderedOtherTypes = Array.from(otherByType.keys()); if (requests.length === 0 && responses.length === 0 && other.length === 0 && blockedCount > 0) { // No requests, but there are DIFC_FILTERED events — add a minimal header @@ -483,7 +491,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { summaryParts.push(`${responses.length} response${responses.length !== 1 ? "s" : ""}`); } for (const type of renderedOtherTypes) { - const count = otherByType[type].length; + const count = otherByType.get(type)?.length || 0; summaryParts.push(`${count} ${type}`); } if (blockedCount > 0) { @@ -533,7 +541,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { callLines.push("| Time | Server | Direction | Details |"); callLines.push("|------|--------|-----------|---------|"); - for (const entry of otherByType[type]) { + for (const entry of otherByType.get(type) || []) { callLines.push(`| ${formatRpcMessageTime(entry.timestamp)} | ${escapeMarkdownTableCell(entry.server_id || "-")} | ${escapeMarkdownTableCell(entry.direction || "-")} | ${escapeMarkdownTableCell(summarizeGenericRpcEntry(entry))} |`); } From 2a6afcdee5e0e4536c1fa7b70cc593a196652d48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:28:19 +0000 Subject: [PATCH 4/7] chore: tidy gateway summary helpers Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 03719b15c3a..e59a4d5062d 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -364,7 +364,7 @@ function summarizeRpcResponseEntry(entry) { const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : {}; const error = payload.error && typeof payload.error === "object" ? payload.error : null; if (error) { - const code = error.code != null ? ` ${error.code}` : ""; + const code = error.code !== null && error.code !== undefined ? ` ${error.code}` : ""; const message = truncateSummaryValue(String(error.message || "Unknown error"), MAX_RPC_SUMMARY_DETAILS_LENGTH); return { status: "error", @@ -430,7 +430,7 @@ function summarizeGenericRpcEntry(entry) { if (payload.params && typeof payload.params === "object" && payload.params.name) { pushPart("tool", payload.params.name); } - if (payload.id != null) { + if (payload.id !== null && payload.id !== undefined) { pushPart("id", payload.id); } if (payload.error && typeof payload.error === "object" && payload.error.message) { @@ -470,12 +470,9 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { /** @type {Map>} */ const otherByType = new Map(); for (const entry of other) { - const existingEntries = otherByType.get(entry.type); - if (existingEntries) { - existingEntries.push(entry); - } else { - otherByType.set(entry.type, [entry]); - } + const entriesForType = otherByType.get(entry.type) || []; + entriesForType.push(entry); + otherByType.set(entry.type, entriesForType); } const renderedOtherTypes = Array.from(otherByType.keys()); From 7effab01b68136c9fabccd31a3f098e10e4762fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:30:28 +0000 Subject: [PATCH 5/7] refactor: simplify gateway summary rows Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index e59a4d5062d..0fe058a117e 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -345,14 +345,15 @@ function escapeMarkdownTableCell(value) { /** * Truncates a string to a maximum length, appending an ellipsis when needed. - * @param {string} value + * @param {unknown} value * @param {number} maxLength * @returns {string} */ function truncateSummaryValue(value, maxLength) { - if (value.length <= maxLength) return value; - if (maxLength < 3) return value.slice(0, Math.max(maxLength, 0)); - return `${value.slice(0, maxLength - 3)}...`; + const text = String(value); + if (text.length <= maxLength) return text; + if (maxLength < 3) return text.slice(0, Math.max(maxLength, 0)); + return `${text.slice(0, maxLength - 3)}...`; } /** @@ -451,6 +452,15 @@ function summarizeGenericRpcEntry(entry) { return truncateSummaryValue(parts.join(" · "), MAX_RPC_SUMMARY_GENERIC_LENGTH); } +/** + * Builds a markdown table row for the RPC message summary. + * @param {Array} cells + * @returns {string} + */ +function buildRpcSummaryRow(cells) { + return `| ${cells.map(cell => escapeMarkdownTableCell(cell)).join(" | ")} |`; +} + /** * Generates a markdown step summary for rpc-messages.jsonl entries (mcpg v0.2.0+ format). * Shows a table of REQUEST entries (tool calls), a count of RESPONSE entries, any other @@ -524,9 +534,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { for (const response of responses) { const { status, details } = summarizeRpcResponseEntry(response); - callLines.push( - `| ${formatRpcMessageTime(response.timestamp)} | ${escapeMarkdownTableCell(response.server_id || "-")} | ${escapeMarkdownTableCell(response.direction || "-")} | ${escapeMarkdownTableCell(status)} | ${escapeMarkdownTableCell(details)} |` - ); + callLines.push(buildRpcSummaryRow([formatRpcMessageTime(response.timestamp), response.server_id || "-", response.direction || "-", status, details])); } callLines.push(""); @@ -539,7 +547,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { callLines.push("|------|--------|-----------|---------|"); for (const entry of otherByType.get(type) || []) { - callLines.push(`| ${formatRpcMessageTime(entry.timestamp)} | ${escapeMarkdownTableCell(entry.server_id || "-")} | ${escapeMarkdownTableCell(entry.direction || "-")} | ${escapeMarkdownTableCell(summarizeGenericRpcEntry(entry))} |`); + callLines.push(buildRpcSummaryRow([formatRpcMessageTime(entry.timestamp), entry.server_id || "-", entry.direction || "-", summarizeGenericRpcEntry(entry)])); } callLines.push(""); From 17be9eff332a2ffdd4614b02846c99087be16c21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 02:32:39 +0000 Subject: [PATCH 6/7] chore: normalize gateway summary truncation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0f9716a4-6a7a-48e5-ad29-5ed816d2c6ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 0fe058a117e..39a85d99f4d 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -351,6 +351,7 @@ function escapeMarkdownTableCell(value) { */ function truncateSummaryValue(value, maxLength) { const text = String(value); + if (maxLength <= 0) return ""; if (text.length <= maxLength) return text; if (maxLength < 3) return text.slice(0, Math.max(maxLength, 0)); return `${text.slice(0, maxLength - 3)}...`; @@ -415,11 +416,11 @@ function summarizeGenericRpcEntry(entry) { const parts = []; const topLevelIgnoredKeys = new Set(["timestamp", "direction", "type", "server_id", "payload"]); const pushPart = (key, value) => { - parts.push(`${key}=${truncateSummaryValue(String(value), MAX_RPC_SUMMARY_DETAILS_LENGTH)}`); + parts.push(`${key}=${truncateSummaryValue(String(value), MAX_RPC_SUMMARY_GENERIC_LENGTH)}`); }; for (const [key, value] of Object.entries(entry)) { - if (topLevelIgnoredKeys.has(key) || value == null || typeof value === "object") continue; + if (topLevelIgnoredKeys.has(key) || value === null || value === undefined || typeof value === "object") continue; pushPart(key, value); } From 8c2c3aeb400871f29098a4af7909c53ea6ed098d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 03:18:02 +0000 Subject: [PATCH 7/7] fix: harden gateway summary markdown rendering Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6f5d85f2-9e80-432c-9e77-7c379f3982fa Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_mcp_gateway_log.cjs | 53 ++++++++++++++++--- .../setup/js/parse_mcp_gateway_log.test.cjs | 20 +++++-- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 39a85d99f4d..029d6900794 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -20,6 +20,8 @@ const { computeEffectiveTokens, getTokenClassWeights, formatET } = require("./ef const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl"; const MAX_RPC_SUMMARY_DETAILS_LENGTH = 120; const MAX_RPC_SUMMARY_GENERIC_LENGTH = 160; +const MAX_RPC_MESSAGE_LABEL_LENGTH = 80; +const TOP_LEVEL_RPC_IGNORED_KEYS = new Set(["timestamp", "direction", "type", "server_id", "payload"]); /** * Formats milliseconds as a human-readable duration string. @@ -343,6 +345,20 @@ function escapeMarkdownTableCell(value) { .trim(); } +/** + * Escapes text for safe use in HTML fragments embedded in markdown. + * @param {unknown} value + * @returns {string} + */ +function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + /** * Truncates a string to a maximum length, appending an ellipsis when needed. * @param {unknown} value @@ -353,10 +369,34 @@ function truncateSummaryValue(value, maxLength) { const text = String(value); if (maxLength <= 0) return ""; if (text.length <= maxLength) return text; - if (maxLength < 3) return text.slice(0, Math.max(maxLength, 0)); + if (maxLength < 4) return text.slice(0, maxLength); return `${text.slice(0, maxLength - 3)}...`; } +/** + * Normalizes an RPC summary label sourced from logs. + * @param {unknown} value + * @param {number} maxLength + * @returns {string} + */ +function normalizeRpcSummaryLabel(value, maxLength = MAX_RPC_MESSAGE_LABEL_LENGTH) { + return truncateSummaryValue( + String(value ?? "-") + .replace(/\s+/g, " ") + .trim() || "-", + maxLength + ); +} + +/** + * Formats an RPC label as HTML code for safe use inside markdown tables. + * @param {unknown} value + * @returns {string} + */ +function formatRpcInlineCodeLabel(value) { + return `${escapeHtml(normalizeRpcSummaryLabel(value))}`; +} + /** * Summarizes an MCP RESPONSE entry for table rendering. * @param {Object} entry @@ -414,13 +454,12 @@ function summarizeRpcResponseEntry(entry) { */ function summarizeGenericRpcEntry(entry) { const parts = []; - const topLevelIgnoredKeys = new Set(["timestamp", "direction", "type", "server_id", "payload"]); const pushPart = (key, value) => { parts.push(`${key}=${truncateSummaryValue(String(value), MAX_RPC_SUMMARY_GENERIC_LENGTH)}`); }; for (const [key, value] of Object.entries(entry)) { - if (topLevelIgnoredKeys.has(key) || value === null || value === undefined || typeof value === "object") continue; + if (TOP_LEVEL_RPC_IGNORED_KEYS.has(key) || value === null || value === undefined || typeof value === "object") continue; pushPart(key, value); } @@ -500,7 +539,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { } for (const type of renderedOtherTypes) { const count = otherByType.get(type)?.length || 0; - summaryParts.push(`${count} ${type}`); + summaryParts.push(`${count} ${escapeHtml(normalizeRpcSummaryLabel(type))}`); } if (blockedCount > 0) { summaryParts.push(`${blockedCount} blocked`); @@ -520,8 +559,8 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { for (const req of requests) { const time = formatRpcMessageTime(req.timestamp); const server = escapeMarkdownTableCell(req.server_id || "-"); - const label = escapeMarkdownTableCell(getRpcRequestLabel(req)); - callLines.push(`| ${time} | ${server} | \`${label}\` |`); + const label = formatRpcInlineCodeLabel(getRpcRequestLabel(req)); + callLines.push(`| ${time} | ${server} | ${label} |`); } callLines.push(""); @@ -542,7 +581,7 @@ function generateRpcMessagesSummary(entries, difcFilteredEvents) { } for (const type of renderedOtherTypes) { - callLines.push(`#### ${type}`); + callLines.push(`#### ${escapeHtml(normalizeRpcSummaryLabel(type))}`); callLines.push(""); callLines.push("| Time | Server | Direction | Details |"); callLines.push("|------|--------|-----------|---------|"); diff --git a/actions/setup/js/parse_mcp_gateway_log.test.cjs b/actions/setup/js/parse_mcp_gateway_log.test.cjs index 8cd3e615899..247f5e4c35c 100644 --- a/actions/setup/js/parse_mcp_gateway_log.test.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.test.cjs @@ -918,7 +918,7 @@ Some content here.`; expect(generateRpcMessagesSummary({ requests: [], responses: [], other: [] }, [])).toBe(""); }); - test("generates details/summary with request count", () => { + test("generates details/summary with request and response counts", () => { const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: sampleResponses, other: [] }, []); expect(summary).toContain("
"); expect(summary).toContain("MCP Gateway Activity (2 requests, 1 response)"); @@ -929,12 +929,18 @@ Some content here.`; const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: [], other: [] }, []); expect(summary).toContain("#### REQUEST"); expect(summary).toContain("| Time | Server | Tool / Method |"); - expect(summary).toContain("`list_issues`"); - expect(summary).toContain("`add_comment`"); + expect(summary).toContain("list_issues"); + expect(summary).toContain("add_comment"); expect(summary).toContain("github"); expect(summary).toContain("safeoutputs"); }); + test("escapes request labels rendered as code", () => { + const requests = [{ timestamp: "2026-01-18T11:10:49Z", direction: "OUT", type: "REQUEST", server_id: "github", payload: { method: "tools/call", params: { name: "list`issues" } } }]; + const summary = generateRpcMessagesSummary({ requests, responses: [], other: [] }, []); + expect(summary).toContain("list`issues<bad>"); + }); + test("formats ISO timestamp as readable date-time", () => { const summary = generateRpcMessagesSummary({ requests: sampleRequests, responses: [], other: [] }, []); expect(summary).toContain("2026-01-18 11:10:49Z"); @@ -981,6 +987,14 @@ Some content here.`; expect(summary).toContain("reason=completed"); }); + test("sanitizes other message type labels in headings and summary", () => { + const other = [{ timestamp: "2026-01-18T11:10:40Z", direction: "OUT", type: "SESSION_\nSTART", server_id: "github", session_id: "abc123" }]; + const summary = generateRpcMessagesSummary({ requests: [], responses: [], other }, []); + expect(summary).toContain("MCP Gateway Activity (1 SESSION_<bad> START)"); + expect(summary).toContain("#### SESSION_<bad> START"); + expect(summary).not.toContain("#### SESSION_\nSTART"); + }); + test("shows minimal header when only DIFC events exist (no requests)", () => { const difcEvents = [{ type: "DIFC_FILTERED", tool_name: "list_issues", reason: "blocked" }]; const summary = generateRpcMessagesSummary({ requests: [], responses: [], other: [] }, difcEvents);