diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 2c1975dfab9..608a43249f0 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -23,13 +23,13 @@ # # This file defines the generated agentic maintenance workflow for this repository. # It runs scheduled cleanup for expiring safe outputs and supports manual maintenance operations. -# +# # This workflow is generated automatically when workflows use expiring safe outputs # or when repository maintenance features are enabled in .github/workflows/aw.json. -# +# # To disable maintenance workflow generation, set in .github/workflows/aw.json: # {"maintenance": false} -# +# # Agentic maintenance docs: # https://github.github.com/gh-aw/reference/ephemerals/#manual-maintenance-operations # diff --git a/actions/setup/js/mcp_server_core.cjs b/actions/setup/js/mcp_server_core.cjs index 7b707a21c4e..35cd0c84905 100644 --- a/actions/setup/js/mcp_server_core.cjs +++ b/actions/setup/js/mcp_server_core.cjs @@ -984,7 +984,11 @@ async function handleMessage(server, req, defaultHandler) { server.replyError(id, -32601, `Method not found: ${method}`); } } catch (e) { - server.replyError(id, -32603, e instanceof Error ? e.message : String(e)); + // Use the error code only if it's a valid JSON-RPC error code (must be a negative integer). + // Subprocess exit codes (positive integers like 1, 2, etc.) must not be used as JSON-RPC + // error codes, as that would produce non-conformant responses (e.g. "code=1"). + const code = e && typeof e === "object" && Number.isInteger(e.code) && e.code < 0 ? e.code : -32603; + server.replyError(id, code, e && e.message ? String(e.message) : "Internal error"); } } diff --git a/actions/setup/js/mcp_server_core.test.cjs b/actions/setup/js/mcp_server_core.test.cjs index e5819d785fc..fb27feedc55 100644 --- a/actions/setup/js/mcp_server_core.test.cjs +++ b/actions/setup/js/mcp_server_core.test.cjs @@ -471,6 +471,127 @@ describe("mcp_server_core.cjs", () => { expect(results).toHaveLength(1); expect(results[0].result.content[0].text).toBe("default handler for no_handler_tool"); }); + + it("should return the code and message from a thrown plain object (not Error instance)", async () => { + const { registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + registerTool(server, { + name: "plain_throw_tool", + description: "A tool that throws a plain object", + inputSchema: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: () => { + throw { code: -32602, message: "ERR_VALIDATION: body required" }; + }, + }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 99, + method: "tools/call", + params: { name: "plain_throw_tool", arguments: { input: "x" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32602); + expect(results[0].error.message).toBe("ERR_VALIDATION: body required"); + expect(results[0].error.message).not.toContain("[object Object]"); + }); + + it("should fall back to -32603 when thrown plain object has no valid error code", async () => { + const { registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + registerTool(server, { + name: "no_code_throw_tool", + description: "A tool that throws a plain object without a code", + inputSchema: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: () => { + throw { message: "something went wrong" }; + }, + }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 100, + method: "tools/call", + params: { name: "no_code_throw_tool", arguments: { input: "x" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32603); + expect(results[0].error.message).toBe("something went wrong"); + }); + + it("should fall back to -32603 when thrown plain object has a positive code", async () => { + const { registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + registerTool(server, { + name: "positive_code_throw_tool", + description: "A tool that throws a plain object with a positive code", + inputSchema: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: () => { + throw { code: 1, message: "process exited with code 1" }; + }, + }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 101, + method: "tools/call", + params: { name: "positive_code_throw_tool", arguments: { input: "x" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32603); + expect(results[0].error.message).toBe("process exited with code 1"); + }); + + it("should use Internal error when thrown plain object has no message property", async () => { + const { registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + registerTool(server, { + name: "no_message_throw_tool", + description: "A tool that throws a plain object without a message", + inputSchema: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: () => { + throw { code: -32602 }; + }, + }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 102, + method: "tools/call", + params: { name: "no_message_throw_tool", arguments: { input: "x" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32602); + expect(results[0].error.message).toBe("Internal error"); + }); + + it("should fall back to -32603 when thrown plain object has a non-integer negative code", async () => { + const { registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + + registerTool(server, { + name: "non_integer_code_throw_tool", + description: "A tool that throws a plain object with a non-integer code", + inputSchema: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: () => { + throw { code: -1.5, message: "fractional code should be ignored" }; + }, + }); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 103, + method: "tools/call", + params: { name: "non_integer_code_throw_tool", arguments: { input: "x" } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32603); + expect(results[0].error.message).toBe("fractional code should be ignored"); + }); }); describe("handleRequest", () => {