From ba531012d6acdaf3d42a2600261b044d6ac51a49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:40:17 +0000 Subject: [PATCH 01/10] Improve tool denial recent call details Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_sdk_driver.test.cjs | 70 +++++++++++++++++++ actions/setup/js/copilot_sdk_session.cjs | 31 +++++++- actions/setup/js/handle_agent_failure.cjs | 62 +++++++++++++++- .../setup/js/handle_agent_failure.test.cjs | 31 ++++++++ 4 files changed, 191 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/copilot_sdk_driver.test.cjs b/actions/setup/js/copilot_sdk_driver.test.cjs index 06e47c350fa..ff1f3ade9d4 100644 --- a/actions/setup/js/copilot_sdk_driver.test.cjs +++ b/actions/setup/js/copilot_sdk_driver.test.cjs @@ -119,6 +119,76 @@ describe("copilot_sdk_driver.cjs", () => { expect(stop).toHaveBeenCalledTimes(1); }); + it("serializes tool.execution_start command details when available", async () => { + const disconnect = vi.fn().mockResolvedValue(undefined); + const stop = vi.fn().mockResolvedValue(undefined); + const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + try { + let onEvent = () => {}; + const session = { + sessionId: "session-tool-start-command", + on: handler => { + onEvent = handler; + }, + sendAndWait: vi.fn().mockImplementation(async () => { + onEvent({ + type: "tool.execution_start", + ephemeral: false, + timestamp: new Date().toISOString(), + data: { + toolName: "bash", + mcpServerName: "terminal", + input: { command: "git status" }, + }, + }); + onEvent({ + type: "assistant.message", + ephemeral: false, + timestamp: new Date().toISOString(), + data: { content: "ok" }, + }); + return { data: { content: "ok" } }; + }), + disconnect, + }; + class FakeCopilotClient { + start = vi.fn().mockResolvedValue(undefined); + createSession = vi.fn().mockResolvedValue(session); + stop = stop; + } + + const result = await runWithCopilotSDK({ + sdkUri: "http://127.0.0.1:3002", + prompt: "test prompt", + logger: () => {}, + sdkModule: { + CopilotClient: FakeCopilotClient, + RuntimeConnection: { forUri: vi.fn(() => ({})) }, + approveAll: () => "allow", + }, + }); + + expect(result.exitCode).toBe(0); + const parsedEvents = stderrWriteSpy.mock.calls + .map(([message]) => { + if (typeof message !== "string" || !message.endsWith("\n")) return null; + try { + return JSON.parse(message.trimEnd()); + } catch { + return null; + } + }) + .filter(Boolean); + const startEvent = parsedEvents.find(event => event.type === "tool.execution_start"); + expect(startEvent).toMatchObject({ + type: "tool.execution_start", + data: { toolName: "bash", mcpServerName: "terminal", command: "git status" }, + }); + } finally { + stderrWriteSpy.mockRestore(); + } + }); + it("resolves exitCode 0 on SDK idle-timeout when output collected and all tool calls complete", async () => { // Regression test: when sendAndWait throws an idle-timeout error but the agent // produced output and all tool calls completed, the driver must return exitCode 0. diff --git a/actions/setup/js/copilot_sdk_session.cjs b/actions/setup/js/copilot_sdk_session.cjs index a2991741161..a44dd41565f 100644 --- a/actions/setup/js/copilot_sdk_session.cjs +++ b/actions/setup/js/copilot_sdk_session.cjs @@ -9,7 +9,7 @@ * * Event mapping: * SDK "user.message" → JSONL "user.message" - * SDK "tool.execution_start" → JSONL "tool.execution_start" (toolName, mcpServerName) + * SDK "tool.execution_start" → JSONL "tool.execution_start" (toolName, mcpServerName, command?) * SDK "tool.execution_complete" → JSONL "tool.execution_complete" (toolName, mcpServerName, success, result) * SDK "assistant.message" → JSONL "assistant.message" (content) * @@ -58,6 +58,32 @@ function extractPromptFromArgs(args) { return null; } +/** + * Best-effort extraction of shell command text from a tool.execution_start event payload. + * @param {any} data + * @returns {string} + */ +function extractToolExecutionStartCommand(data) { + if (!data || typeof data !== "object") return ""; + const directCandidates = [data.command, data.input, data.arguments, data.args]; + for (const candidate of directCandidates) { + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } + } + const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; + for (const candidate of nestedCandidates) { + if (!candidate || typeof candidate !== "object") continue; + if (typeof candidate.command === "string" && candidate.command.trim()) { + return candidate.command.trim(); + } + if (typeof candidate.cmd === "string" && candidate.cmd.trim()) { + return candidate.cmd.trim(); + } + } + return ""; +} + /** * Run a Copilot agentic session using the @github/copilot-sdk. * @@ -256,10 +282,11 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c const toolName = event.data?.toolName ?? "unknown"; const mcpServerName = event.data?.mcpServerName ?? ""; const toolCallId = event.data?.toolCallId; + const command = extractToolExecutionStartCommand(event.data); if (toolCallId) { pendingToolCalls.set(toolCallId, { toolName, mcpServerName }); } - writeEvent("tool.execution_start", { toolName, mcpServerName }, event.timestamp); + writeEvent("tool.execution_start", command ? { toolName, mcpServerName, command } : { toolName, mcpServerName }, event.timestamp); break; } diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index d9c4081842b..2e7b5270122 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1174,6 +1174,66 @@ function normalizeDeniedPermissionCommand(command) { return cmd; } +/** + * Collapse tool call details to a compact single-line preview. + * @param {string} value + * @param {number} [maxLen] + * @returns {string} + */ +function normalizeToolCallPreview(value, maxLen = 120) { + const singleLine = String(value || "") + .replace(/\s+/g, " ") + .trim(); + if (!singleLine) return ""; + if (singleLine.length <= maxLen) return singleLine; + return `${singleLine.slice(0, maxLen - 3)}...`; +} + +/** + * Best-effort extraction of a shell command preview from a tool.execution_start payload. + * @param {Record} data + * @returns {string} + */ +function extractShellCommandPreview(data) { + if (!data || typeof data !== "object") return ""; + const directCandidates = [data.command, data.input, data.arguments, data.args]; + for (const candidate of directCandidates) { + if (typeof candidate === "string" && candidate.trim()) { + return normalizeToolCallPreview(candidate); + } + } + + const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; + for (const candidate of nestedCandidates) { + if (!candidate || typeof candidate !== "object") continue; + if (typeof candidate.command === "string" && candidate.command.trim()) { + return normalizeToolCallPreview(candidate.command); + } + if (typeof candidate.cmd === "string" && candidate.cmd.trim()) { + return normalizeToolCallPreview(candidate.cmd); + } + } + + return ""; +} + +/** + * Format a compact display value for a recent tool call entry. + * @param {string} toolName + * @param {string} mcpServerName + * @param {Record} data + * @returns {string} + */ +function formatRecentToolCall(toolName, mcpServerName, data) { + const base = mcpServerName ? `${mcpServerName}.${toolName}` : toolName; + const normalizedTool = String(toolName || "").toLowerCase(); + if (normalizedTool !== "bash" && normalizedTool !== "shell") { + return base; + } + const commandPreview = extractShellCommandPreview(data); + return commandPreview ? `${base}(${commandPreview})` : base; +} + /** * Load missing_tool messages from agent output. * Returns an empty array when the output file doesn't exist, cannot be parsed, or has no missing_tool items. @@ -1320,7 +1380,7 @@ function loadToolDenialsExceededEvents() { const toolName = typeof parsed.data.toolName === "string" ? parsed.data.toolName.trim() : ""; if (toolName) { const mcpServerName = typeof parsed.data.mcpServerName === "string" ? parsed.data.mcpServerName.trim() : ""; - recentToolCalls.push(mcpServerName ? `${mcpServerName}.${toolName}` : toolName); + recentToolCalls.push(formatRecentToolCall(toolName, mcpServerName, parsed.data)); if (recentToolCalls.length > 5) recentToolCalls.shift(); } continue; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index e142fe5d1c5..c7d6fe9dc10 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -3272,6 +3272,37 @@ describe("handle_agent_failure", () => { }, ]); }); + + it("captures shell command details for recent bash tool calls", () => { + const sessionDir = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state", "session-1"); + fs.mkdirSync(sessionDir, { recursive: true }); + fs.writeFileSync( + path.join(sessionDir, "events.jsonl"), + [ + JSON.stringify({ + type: "tool.execution_start", + timestamp: "2026-06-06T00:00:00Z", + data: { toolName: "bash", mcpServerName: "terminal", command: "cd /home/runner/work/gh-aw/gh-aw && git diff --name-only" }, + }), + JSON.stringify({ + type: "guard.tool_denials_exceeded", + timestamp: "2026-06-06T00:00:01Z", + data: { denialCount: 5, threshold: 5, reason: "permission denied: bash" }, + }), + ].join("\n") + "\n" + ); + + const events = loadToolDenialsExceededEvents(); + expect(events).toEqual([ + { + denialCount: 5, + threshold: 5, + reason: "permission denied: bash", + recentToolCalls: ["terminal.bash(cd /home/runner/work/gh-aw/gh-aw && git diff --name-only)"], + timestamp: "2026-06-06T00:00:01Z", + }, + ]); + }); }); // ────────────────────────────────────────────────────── From 0483ae407271d6390348f32ee303566dee9a25cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:42:12 +0000 Subject: [PATCH 02/10] Refactor tool call command extraction Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_sdk_session.cjs | 29 ++------------------- actions/setup/js/handle_agent_failure.cjs | 22 ++-------------- actions/setup/js/tool_call_details.cjs | 31 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 47 deletions(-) create mode 100644 actions/setup/js/tool_call_details.cjs diff --git a/actions/setup/js/copilot_sdk_session.cjs b/actions/setup/js/copilot_sdk_session.cjs index a44dd41565f..57650134e4d 100644 --- a/actions/setup/js/copilot_sdk_session.cjs +++ b/actions/setup/js/copilot_sdk_session.cjs @@ -29,6 +29,7 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); const { buildCopilotSDKPermissionHandler, getEnvPositiveIntOrDefault, parseMaxToolDenialsLimit, MAX_TOOL_DENIALS_DEFAULT } = require("./copilot_sdk_permissions.cjs"); +const { extractShellCommandFromToolData } = require("./tool_call_details.cjs"); // Default timeout for a single sendAndWait call: 10 minutes. // This is intentionally generous — the headless Copilot CLI has its own internal @@ -58,32 +59,6 @@ function extractPromptFromArgs(args) { return null; } -/** - * Best-effort extraction of shell command text from a tool.execution_start event payload. - * @param {any} data - * @returns {string} - */ -function extractToolExecutionStartCommand(data) { - if (!data || typeof data !== "object") return ""; - const directCandidates = [data.command, data.input, data.arguments, data.args]; - for (const candidate of directCandidates) { - if (typeof candidate === "string" && candidate.trim()) { - return candidate.trim(); - } - } - const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; - for (const candidate of nestedCandidates) { - if (!candidate || typeof candidate !== "object") continue; - if (typeof candidate.command === "string" && candidate.command.trim()) { - return candidate.command.trim(); - } - if (typeof candidate.cmd === "string" && candidate.cmd.trim()) { - return candidate.cmd.trim(); - } - } - return ""; -} - /** * Run a Copilot agentic session using the @github/copilot-sdk. * @@ -282,7 +257,7 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c const toolName = event.data?.toolName ?? "unknown"; const mcpServerName = event.data?.mcpServerName ?? ""; const toolCallId = event.data?.toolCallId; - const command = extractToolExecutionStartCommand(event.data); + const command = extractShellCommandFromToolData(event.data); if (toolCallId) { pendingToolCalls.set(toolCallId, { toolName, mcpServerName }); } diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 2e7b5270122..322d720ab96 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -16,6 +16,7 @@ const { formatAICCredits } = require("./daily_aic_workflow_helpers.cjs"); const { formatAIC } = require("./model_costs.cjs"); const { parseTokenUsageJsonl, generateTokenUsageSummary } = require("./parse_mcp_gateway_log.cjs"); const { readDedupedTokenUsage, TOKEN_USAGE_PATHS } = require("./parse_token_usage.cjs"); +const { extractShellCommandFromToolData } = require("./tool_call_details.cjs"); const fs = require("fs"); const os = require("os"); const path = require("path"); @@ -1195,26 +1196,7 @@ function normalizeToolCallPreview(value, maxLen = 120) { * @returns {string} */ function extractShellCommandPreview(data) { - if (!data || typeof data !== "object") return ""; - const directCandidates = [data.command, data.input, data.arguments, data.args]; - for (const candidate of directCandidates) { - if (typeof candidate === "string" && candidate.trim()) { - return normalizeToolCallPreview(candidate); - } - } - - const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; - for (const candidate of nestedCandidates) { - if (!candidate || typeof candidate !== "object") continue; - if (typeof candidate.command === "string" && candidate.command.trim()) { - return normalizeToolCallPreview(candidate.command); - } - if (typeof candidate.cmd === "string" && candidate.cmd.trim()) { - return normalizeToolCallPreview(candidate.cmd); - } - } - - return ""; + return normalizeToolCallPreview(extractShellCommandFromToolData(data)); } /** diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs new file mode 100644 index 00000000000..1bb166c28cf --- /dev/null +++ b/actions/setup/js/tool_call_details.cjs @@ -0,0 +1,31 @@ +// @ts-check + +/** + * Best-effort extraction of shell command text from a tool.execution_start payload. + * @param {any} data + * @returns {string} + */ +function extractShellCommandFromToolData(data) { + if (!data || typeof data !== "object") return ""; + const directCandidates = [data.command, data.input, data.arguments, data.args]; + for (const candidate of directCandidates) { + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } + } + const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; + for (const candidate of nestedCandidates) { + if (!candidate || typeof candidate !== "object") continue; + if (typeof candidate.command === "string" && candidate.command.trim()) { + return candidate.command.trim(); + } + if (typeof candidate.cmd === "string" && candidate.cmd.trim()) { + return candidate.cmd.trim(); + } + } + return ""; +} + +module.exports = { + extractShellCommandFromToolData, +}; From b1f478e50622f744aa30bf20723249a0d6945544 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:43:58 +0000 Subject: [PATCH 03/10] Address validation feedback on tool details Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 2 +- actions/setup/js/tool_call_details.cjs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 322d720ab96..f4c5fe2d2e4 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1208,7 +1208,7 @@ function extractShellCommandPreview(data) { */ function formatRecentToolCall(toolName, mcpServerName, data) { const base = mcpServerName ? `${mcpServerName}.${toolName}` : toolName; - const normalizedTool = String(toolName || "").toLowerCase(); + const normalizedTool = toolName.toLowerCase(); if (normalizedTool !== "bash" && normalizedTool !== "shell") { return base; } diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index 1bb166c28cf..1fa1a80105f 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -7,14 +7,11 @@ */ function extractShellCommandFromToolData(data) { if (!data || typeof data !== "object") return ""; - const directCandidates = [data.command, data.input, data.arguments, data.args]; - for (const candidate of directCandidates) { + const candidates = [data.command, data.input, data.arguments, data.args, data.toolInput, data.parameters]; + for (const candidate of candidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate.trim(); } - } - const nestedCandidates = [data.input, data.arguments, data.args, data.toolInput, data.parameters]; - for (const candidate of nestedCandidates) { if (!candidate || typeof candidate !== "object") continue; if (typeof candidate.command === "string" && candidate.command.trim()) { return candidate.command.trim(); From 8100e51de0c62f2cf18369509db61f4564e11cb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:45:44 +0000 Subject: [PATCH 04/10] Document and centralize shell preview logic Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 3 ++- actions/setup/js/tool_call_details.cjs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index f4c5fe2d2e4..5618930b961 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -30,6 +30,7 @@ const DEFAULT_OTEL_JSONL_PATH = "/tmp/gh-aw/otel.jsonl"; const FAILURE_CATEGORIES_PATH = "/tmp/gh-aw/failure_categories.json"; const GITHUB_API_VERSION = "2022-11-28"; const COPILOT_SESSION_STATE_DIR = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state"); +const RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW = new Set(["bash", "shell"]); // Engine-side 429/rate-limit signatures: // - HTTP 429 accompanied by "too many requests"/"rate limit" phrasing // - provider error codes like rate_limit_error / rate_limit_exceeded @@ -1209,7 +1210,7 @@ function extractShellCommandPreview(data) { function formatRecentToolCall(toolName, mcpServerName, data) { const base = mcpServerName ? `${mcpServerName}.${toolName}` : toolName; const normalizedTool = toolName.toLowerCase(); - if (normalizedTool !== "bash" && normalizedTool !== "shell") { + if (!RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW.has(normalizedTool)) { return base; } const commandPreview = extractShellCommandPreview(data); diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index 1fa1a80105f..de49a5d0adf 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -7,6 +7,8 @@ */ function extractShellCommandFromToolData(data) { if (!data || typeof data !== "object") return ""; + // Priority order prefers top-level command-like fields emitted by tool wrappers, + // then object-shaped payloads used by MCP/SDK tool schemas. const candidates = [data.command, data.input, data.arguments, data.args, data.toolInput, data.parameters]; for (const candidate of candidates) { if (typeof candidate === "string" && candidate.trim()) { From 469120820a8bea74404403a5f551034d91cd81b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:47:28 +0000 Subject: [PATCH 05/10] Polish naming in shell detail helpers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 4 ++-- actions/setup/js/tool_call_details.cjs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 5618930b961..aefd2e93c77 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1209,8 +1209,8 @@ function extractShellCommandPreview(data) { */ function formatRecentToolCall(toolName, mcpServerName, data) { const base = mcpServerName ? `${mcpServerName}.${toolName}` : toolName; - const normalizedTool = toolName.toLowerCase(); - if (!RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW.has(normalizedTool)) { + const normalizedToolName = toolName.toLowerCase(); + if (!RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW.has(normalizedToolName)) { return base; } const commandPreview = extractShellCommandPreview(data); diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index de49a5d0adf..ce6288e5351 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -9,8 +9,8 @@ function extractShellCommandFromToolData(data) { if (!data || typeof data !== "object") return ""; // Priority order prefers top-level command-like fields emitted by tool wrappers, // then object-shaped payloads used by MCP/SDK tool schemas. - const candidates = [data.command, data.input, data.arguments, data.args, data.toolInput, data.parameters]; - for (const candidate of candidates) { + const commandFieldCandidates = [data.command, data.input, data.arguments, data.args, data.toolInput, data.parameters]; + for (const candidate of commandFieldCandidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate.trim(); } From eff70e42f83b897e8a192404441b6fb1092c2853 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:49:12 +0000 Subject: [PATCH 06/10] Refactor tool execution start event payload build Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_sdk_session.cjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/copilot_sdk_session.cjs b/actions/setup/js/copilot_sdk_session.cjs index 57650134e4d..95db12e0174 100644 --- a/actions/setup/js/copilot_sdk_session.cjs +++ b/actions/setup/js/copilot_sdk_session.cjs @@ -261,7 +261,9 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c if (toolCallId) { pendingToolCalls.set(toolCallId, { toolName, mcpServerName }); } - writeEvent("tool.execution_start", command ? { toolName, mcpServerName, command } : { toolName, mcpServerName }, event.timestamp); + const eventData = { toolName, mcpServerName }; + if (command) eventData.command = command; + writeEvent("tool.execution_start", eventData, event.timestamp); break; } From 8a619d8771c38297a96a1b687ab71852fd46c9b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:51:15 +0000 Subject: [PATCH 07/10] Tighten helper constants and arguments access Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 3 ++- actions/setup/js/tool_call_details.cjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index aefd2e93c77..a00091f2b42 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -31,6 +31,7 @@ const FAILURE_CATEGORIES_PATH = "/tmp/gh-aw/failure_categories.json"; const GITHUB_API_VERSION = "2022-11-28"; const COPILOT_SESSION_STATE_DIR = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state"); const RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW = new Set(["bash", "shell"]); +const ELLIPSIS_LENGTH = 3; // Engine-side 429/rate-limit signatures: // - HTTP 429 accompanied by "too many requests"/"rate limit" phrasing // - provider error codes like rate_limit_error / rate_limit_exceeded @@ -1188,7 +1189,7 @@ function normalizeToolCallPreview(value, maxLen = 120) { .trim(); if (!singleLine) return ""; if (singleLine.length <= maxLen) return singleLine; - return `${singleLine.slice(0, maxLen - 3)}...`; + return `${singleLine.slice(0, maxLen - ELLIPSIS_LENGTH)}...`; } /** diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index ce6288e5351..95fb55ef79e 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -9,7 +9,7 @@ function extractShellCommandFromToolData(data) { if (!data || typeof data !== "object") return ""; // Priority order prefers top-level command-like fields emitted by tool wrappers, // then object-shaped payloads used by MCP/SDK tool schemas. - const commandFieldCandidates = [data.command, data.input, data.arguments, data.args, data.toolInput, data.parameters]; + const commandFieldCandidates = [data.command, data.input, data["arguments"], data.args, data.toolInput, data.parameters]; for (const candidate of commandFieldCandidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate.trim(); From 2913fb4d34b32e42ae531427a6a68f1c8236d6bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:53:10 +0000 Subject: [PATCH 08/10] Improve helper robustness and ellipsis constants Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 5 +++-- actions/setup/js/tool_call_details.cjs | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index a00091f2b42..707ce0aa74d 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -31,7 +31,8 @@ const FAILURE_CATEGORIES_PATH = "/tmp/gh-aw/failure_categories.json"; const GITHUB_API_VERSION = "2022-11-28"; const COPILOT_SESSION_STATE_DIR = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state"); const RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW = new Set(["bash", "shell"]); -const ELLIPSIS_LENGTH = 3; +const ELLIPSIS = "..."; +const ELLIPSIS_LENGTH = ELLIPSIS.length; // Engine-side 429/rate-limit signatures: // - HTTP 429 accompanied by "too many requests"/"rate limit" phrasing // - provider error codes like rate_limit_error / rate_limit_exceeded @@ -1189,7 +1190,7 @@ function normalizeToolCallPreview(value, maxLen = 120) { .trim(); if (!singleLine) return ""; if (singleLine.length <= maxLen) return singleLine; - return `${singleLine.slice(0, maxLen - ELLIPSIS_LENGTH)}...`; + return `${singleLine.slice(0, maxLen - ELLIPSIS_LENGTH)}${ELLIPSIS}`; } /** diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index 95fb55ef79e..9c1958c5aaf 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -9,7 +9,14 @@ function extractShellCommandFromToolData(data) { if (!data || typeof data !== "object") return ""; // Priority order prefers top-level command-like fields emitted by tool wrappers, // then object-shaped payloads used by MCP/SDK tool schemas. - const commandFieldCandidates = [data.command, data.input, data["arguments"], data.args, data.toolInput, data.parameters]; + /** @type {Array} */ + const commandFieldCandidates = []; + if ("command" in data) commandFieldCandidates.push(data.command); + if ("input" in data) commandFieldCandidates.push(data.input); + if ("arguments" in data) commandFieldCandidates.push(data["arguments"]); + if ("args" in data) commandFieldCandidates.push(data.args); + if ("toolInput" in data) commandFieldCandidates.push(data.toolInput); + if ("parameters" in data) commandFieldCandidates.push(data.parameters); for (const candidate of commandFieldCandidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate.trim(); From 5f011aa3412d0f2e954c0e6bf1b5f0c3c1fb72ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:54:56 +0000 Subject: [PATCH 09/10] Guard tool-name normalization in formatter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 2 +- actions/setup/js/tool_call_details.cjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 707ce0aa74d..258f661b40d 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1211,7 +1211,7 @@ function extractShellCommandPreview(data) { */ function formatRecentToolCall(toolName, mcpServerName, data) { const base = mcpServerName ? `${mcpServerName}.${toolName}` : toolName; - const normalizedToolName = toolName.toLowerCase(); + const normalizedToolName = typeof toolName === "string" ? toolName.toLowerCase() : ""; if (!RECENT_TOOL_CALLS_WITH_COMMAND_PREVIEW.has(normalizedToolName)) { return base; } diff --git a/actions/setup/js/tool_call_details.cjs b/actions/setup/js/tool_call_details.cjs index 9c1958c5aaf..dc51ed0c68d 100644 --- a/actions/setup/js/tool_call_details.cjs +++ b/actions/setup/js/tool_call_details.cjs @@ -13,7 +13,7 @@ function extractShellCommandFromToolData(data) { const commandFieldCandidates = []; if ("command" in data) commandFieldCandidates.push(data.command); if ("input" in data) commandFieldCandidates.push(data.input); - if ("arguments" in data) commandFieldCandidates.push(data["arguments"]); + if ("arguments" in data) commandFieldCandidates.push(data.arguments); if ("args" in data) commandFieldCandidates.push(data.args); if ("toolInput" in data) commandFieldCandidates.push(data.toolInput); if ("parameters" in data) commandFieldCandidates.push(data.parameters); From 286e649f4f531bdad945811015531b47c9769d94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:40:58 +0000 Subject: [PATCH 10/10] fix review feedback for command preview safety and typing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_sdk_session.cjs | 3 +- actions/setup/js/handle_agent_failure.cjs | 1 + .../setup/js/handle_agent_failure.test.cjs | 31 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/copilot_sdk_session.cjs b/actions/setup/js/copilot_sdk_session.cjs index 95db12e0174..9f3129c43f0 100644 --- a/actions/setup/js/copilot_sdk_session.cjs +++ b/actions/setup/js/copilot_sdk_session.cjs @@ -261,8 +261,7 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c if (toolCallId) { pendingToolCalls.set(toolCallId, { toolName, mcpServerName }); } - const eventData = { toolName, mcpServerName }; - if (command) eventData.command = command; + const eventData = command ? { toolName, mcpServerName, command } : { toolName, mcpServerName }; writeEvent("tool.execution_start", eventData, event.timestamp); break; } diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 258f661b40d..08d45e45dd0 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1186,6 +1186,7 @@ function normalizeDeniedPermissionCommand(command) { */ function normalizeToolCallPreview(value, maxLen = 120) { const singleLine = String(value || "") + .replace(/`/g, "'") .replace(/\s+/g, " ") .trim(); if (!singleLine) return ""; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index c7d6fe9dc10..372b10297e5 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -3303,6 +3303,37 @@ describe("handle_agent_failure", () => { }, ]); }); + + it("sanitizes backticks in shell command previews", () => { + const sessionDir = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state", "session-1"); + fs.mkdirSync(sessionDir, { recursive: true }); + fs.writeFileSync( + path.join(sessionDir, "events.jsonl"), + [ + JSON.stringify({ + type: "tool.execution_start", + timestamp: "2026-06-06T00:00:00Z", + data: { toolName: "bash", mcpServerName: "terminal", command: "echo `hostname` && echo ok" }, + }), + JSON.stringify({ + type: "guard.tool_denials_exceeded", + timestamp: "2026-06-06T00:00:01Z", + data: { denialCount: 5, threshold: 5, reason: "permission denied: bash" }, + }), + ].join("\n") + "\n" + ); + + const events = loadToolDenialsExceededEvents(); + expect(events).toEqual([ + { + denialCount: 5, + threshold: 5, + reason: "permission denied: bash", + recentToolCalls: ["terminal.bash(echo 'hostname' && echo ok)"], + timestamp: "2026-06-06T00:00:01Z", + }, + ]); + }); }); // ──────────────────────────────────────────────────────