From 4c14cccb787eeb2206a7b0f0144067940ca1f16d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:38:22 +0000 Subject: [PATCH 1/2] fix(create_issue): prevent content-in-title via prompt, fallback, and minLength validation - safe_outputs_auto_create_issue.md: add explicit title/body guidance so auto-injected create_issue runs don't encourage agents to put report content in the title field - create_issue.cjs / safe_outputs_handlers.cjs: change the no-title fallback from using the *entire* body to using only the first non-empty line (stripping any markdown heading marker), keeping the title concise - intent_probe.cjs: mirror the same first-line fallback so probe-detection sees the same effective title as the actual handler - mcp_scripts_mcp_server_http.cjs: add missing validateStringMinLengths call (already present in mcp_server_core.cjs) so the minLength:20 constraint on body is enforced on the HTTP MCP transport path as well" Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_issue.cjs | 8 +++++++- actions/setup/js/intent_probe.cjs | 7 ++++++- actions/setup/js/mcp_scripts_mcp_server_http.cjs | 9 ++++++++- actions/setup/js/safe_outputs_handlers.cjs | 8 +++++++- actions/setup/md/safe_outputs_auto_create_issue.md | 6 ++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 385579ca33e..7daf568e257 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -778,7 +778,13 @@ async function main(config = {}) { const bodyLines = processedBody.split("\n"); if (!title) { - title = message.body ?? "Agent Output"; + // Use the first non-empty line of the body as the title fallback rather than + // the entire body, so the title stays concise and the body remains intact. + const firstBodyLine = (message.body ?? "") + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + title = firstBodyLine || "Agent Output"; } // Sanitize title for Unicode security and remove any duplicate prefixes diff --git a/actions/setup/js/intent_probe.cjs b/actions/setup/js/intent_probe.cjs index fdef70a6949..a3c36faaeea 100644 --- a/actions/setup/js/intent_probe.cjs +++ b/actions/setup/js/intent_probe.cjs @@ -38,7 +38,12 @@ function resolveIssueTitleForValidation(entry) { return entry.title; } if (typeof entry.body === "string" && entry.body.trim()) { - return entry.body; + // Mirror create_issue's fallback: use the first non-empty line of the body. + const firstBodyLine = entry.body + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + if (firstBodyLine) return firstBodyLine; } // Mirror create_issue's own fallback so probe validation sees the same // effective title the handler would ultimately use when both fields are absent. diff --git a/actions/setup/js/mcp_scripts_mcp_server_http.cjs b/actions/setup/js/mcp_scripts_mcp_server_http.cjs index 34271773174..380b6c66e5e 100644 --- a/actions/setup/js/mcp_scripts_mcp_server_http.cjs +++ b/actions/setup/js/mcp_scripts_mcp_server_http.cjs @@ -24,7 +24,7 @@ require("./shim.cjs"); const { randomUUID } = require("crypto"); const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); -const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError } = require("./mcp_scripts_validation.cjs"); +const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError, validateStringMinLengths } = require("./mcp_scripts_validation.cjs"); const { generateEnhancedErrorMessage } = require("./mcp_enhanced_errors.cjs"); const { createLogger } = require("./mcp_logger.cjs"); const { bootstrapMCPScriptsServer, cleanupConfigFile } = require("./mcp_scripts_bootstrap.cjs"); @@ -100,6 +100,13 @@ function createMCPServer(configPath, options = {}) { throw new Error(buildStringLengthValidationError(tool.name, oversized)); } + // Validate minLength constraints from the schema. + const tooShort = validateStringMinLengths(args, tool.inputSchema); + if (tooShort.length) { + const details = tooShort.map(v => `'${v.field}' is too short (minimum ${v.minLength} characters, got ${v.actualLength})`).join(", "); + throw new Error(`Invalid arguments: ${details}`); + } + // Call the handler const result = await Promise.resolve(tool.handler(args)); logger.debug(`Handler returned for tool: ${tool.name}`); diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index eac9c107b6b..021cbf8fe9d 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1697,7 +1697,13 @@ function createHandlers(server, appendSafeOutput, config = {}) { let resolvedTitle = entry.title?.trim() || ""; if (!resolvedTitle) { - resolvedTitle = entry.body?.trim() || "Agent Output"; + // Use the first non-empty line of the body as the title fallback rather than + // the entire body, so the title stays concise and the body remains intact. + const firstBodyLine = (entry.body || "") + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + resolvedTitle = firstBodyLine || "Agent Output"; } resolvedTitle = applyTitlePrefix(sanitizeTitle(resolvedTitle, createIssueTitlePrefix), createIssueTitlePrefix); diff --git a/actions/setup/md/safe_outputs_auto_create_issue.md b/actions/setup/md/safe_outputs_auto_create_issue.md index cd28d964e14..448a12f52ef 100644 --- a/actions/setup/md/safe_outputs_auto_create_issue.md +++ b/actions/setup/md/safe_outputs_auto_create_issue.md @@ -1,2 +1,8 @@ **IMPORTANT**: Report your findings or results by creating a GitHub issue using the create_issue tool. If you have no meaningful results to report, call the noop tool instead. + +When calling create_issue: +- `title`: a short, descriptive summary (aim for one sentence, ≤ 100 characters) +- `body`: the full content — details, findings, context, and any supporting information + +Do **not** put all the content in the title; the title is a headline, and the body is where the substance goes. From 3fb2beb5205a8c77cb48caa66260ef9e38783bfe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:46:18 +0000 Subject: [PATCH 2/2] test(create_issue): add multiline fallback regressions and HTTP minLength coverage Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_issue.test.cjs | 14 ++++++++ .../setup/js/mcp_scripts_mcp_server_http.cjs | 9 +---- .../js/mcp_scripts_mcp_server_http.test.cjs | 16 ++++++++- .../setup/js/safe_outputs_handlers.test.cjs | 36 +++++++++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/create_issue.test.cjs b/actions/setup/js/create_issue.test.cjs index 3d20ea4214a..236b7c7407b 100644 --- a/actions/setup/js/create_issue.test.cjs +++ b/actions/setup/js/create_issue.test.cjs @@ -138,6 +138,20 @@ describe("create_issue", () => { ); }); + it("should use the first meaningful body line as title when title is missing", async () => { + const handler = await main({}); + const body = "\n\n## Incident Summary\n\nFull details line 1\nFull details line 2"; + const result = await handler({ body }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Incident Summary", + body: expect.stringContaining("Full details line 1\nFull details line 2"), + }) + ); + }); + it("should use 'Agent Output' as title when both title and body are missing", async () => { const handler = await main({}); const result = await handler({}); diff --git a/actions/setup/js/mcp_scripts_mcp_server_http.cjs b/actions/setup/js/mcp_scripts_mcp_server_http.cjs index 380b6c66e5e..34271773174 100644 --- a/actions/setup/js/mcp_scripts_mcp_server_http.cjs +++ b/actions/setup/js/mcp_scripts_mcp_server_http.cjs @@ -24,7 +24,7 @@ require("./shim.cjs"); const { randomUUID } = require("crypto"); const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs"); -const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError, validateStringMinLengths } = require("./mcp_scripts_validation.cjs"); +const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError } = require("./mcp_scripts_validation.cjs"); const { generateEnhancedErrorMessage } = require("./mcp_enhanced_errors.cjs"); const { createLogger } = require("./mcp_logger.cjs"); const { bootstrapMCPScriptsServer, cleanupConfigFile } = require("./mcp_scripts_bootstrap.cjs"); @@ -100,13 +100,6 @@ function createMCPServer(configPath, options = {}) { throw new Error(buildStringLengthValidationError(tool.name, oversized)); } - // Validate minLength constraints from the schema. - const tooShort = validateStringMinLengths(args, tool.inputSchema); - if (tooShort.length) { - const details = tooShort.map(v => `'${v.field}' is too short (minimum ${v.minLength} characters, got ${v.actualLength})`).join(", "); - throw new Error(`Invalid arguments: ${details}`); - } - // Call the handler const result = await Promise.resolve(tool.handler(args)); logger.debug(`Handler returned for tool: ${tool.name}`); diff --git a/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs b/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs index 7e8340fbcf5..5b9e9a171e7 100644 --- a/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs +++ b/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs @@ -40,7 +40,12 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => { serverName: "http-integration-test-server", version: "1.0.0", tools: [ - { name: "echo_tool", description: "Echoes the input message", inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo" } }, required: ["message"] }, handler: "echo-handler.cjs" }, + { + name: "echo_tool", + description: "Echoes the input message", + inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo", minLength: 20 } }, required: ["message"] }, + handler: "echo-handler.cjs", + }, ], }) ), @@ -146,6 +151,15 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => { response = await makeRequest({ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "echo_tool", arguments: {} } }, headers); (expect(response.status).toBe(200), expect(response.data.error).toBeDefined(), expect(response.data.error.message).toContain("not allowed")); }), + it("should enforce minLength from tool schema on HTTP calls", async () => { + const headers = sessionId ? { "Mcp-Session-Id": sessionId } : {}, + response = await makeRequest({ jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "echo_tool", arguments: { message: "too short" } } }, headers); + (expect(response.status).toBe(200), + expect(response.data.error).toBeDefined(), + expect(response.data.error.code).toBe(-32602), + expect(response.data.error.message).toContain("Invalid arguments"), + expect(response.data.error.message).toContain("minimum 20 characters")); + }), afterAll(async () => { (serverProcess && (serverProcess.kill("SIGTERM"), diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index a30d63a6ab5..e1c94c28134 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -105,6 +105,8 @@ describe("safe_outputs_handlers", () => { it("resolves issue title using the create_issue fallback order", () => { expect(resolveIssueTitleForValidation({ title: "Real title", body: "Ignored body" })).toBe("Real title"); expect(resolveIssueTitleForValidation({ body: "Body title" })).toBe("Body title"); + expect(resolveIssueTitleForValidation({ body: "\n\n## Incident Summary\n\nBody details" })).toBe("Incident Summary"); + expect(resolveIssueTitleForValidation({ body: " \n\n " })).toBe("Agent Output"); expect(resolveIssueTitleForValidation({})).toBe("Agent Output"); }); @@ -2305,6 +2307,40 @@ describe("safe_outputs_handlers", () => { }); describe("createIssueHandler", () => { + it("should deduplicate against fallback title derived from first meaningful body line", () => { + const h = createHandlers(mockServer, mockAppendSafeOutput, { + create_issue: { + deduplicate_by_title: true, + }, + }); + + const first = h.createIssueHandler({ body: "\n\n## Incident Summary\n\nBody A details" }); + const second = h.createIssueHandler({ title: "Incident Summary", body: "Body B details" }); + + const firstResponse = JSON.parse(first.content[0].text); + const secondResponse = JSON.parse(second.content[0].text); + expect(firstResponse.result).toBe("success"); + expect(secondResponse.result).toBe("duplicate_dropped"); + const droppedEntry = mockAppendSafeOutput.mock.calls[1][0]; + expect(droppedEntry._dropped_duplicate_by_title).toBe(true); + }); + + it("should fall back to Agent Output when title and body are blank", () => { + const h = createHandlers(mockServer, mockAppendSafeOutput, { + create_issue: { + deduplicate_by_title: true, + }, + }); + + const first = h.createIssueHandler({ body: " \n\n " }); + const second = h.createIssueHandler({ title: "Agent Output", body: "Real details for duplicate check" }); + + const firstResponse = JSON.parse(first.content[0].text); + const secondResponse = JSON.parse(second.content[0].text); + expect(firstResponse.result).toBe("success"); + expect(secondResponse.result).toBe("duplicate_dropped"); + }); + it("should append create_issue entry when dedup is disabled", () => { handlers.createIssueHandler({ title: "Issue A", body: "Body A" }); handlers.createIssueHandler({ title: "Issue A", body: "Body A again" });