diff --git a/Core/PeekabooCore/Sources/PeekabooAgentRuntime/MCP/Tools/ShellTool.swift b/Core/PeekabooCore/Sources/PeekabooAgentRuntime/MCP/Tools/ShellTool.swift index d896a3e2b..3c30cd025 100644 --- a/Core/PeekabooCore/Sources/PeekabooAgentRuntime/MCP/Tools/ShellTool.swift +++ b/Core/PeekabooCore/Sources/PeekabooAgentRuntime/MCP/Tools/ShellTool.swift @@ -65,10 +65,12 @@ public struct ShellTool: MCPTool { do { try process.run() - process.waitUntilExit() - let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + // Either pipe can fill while the other waits for EOF, so drain both concurrently. + async let outputRead = outputPipe.fileHandleForReading.readDataToEndOfFile() + async let errorRead = errorPipe.fileHandleForReading.readDataToEndOfFile() + let (outputData, errorData) = await (outputRead, errorRead) + process.waitUntilExit() let output = String(data: outputData, encoding: .utf8) ?? "" let error = String(data: errorData, encoding: .utf8) ?? "" diff --git a/Core/PeekabooCore/Tests/PeekabooTests/MCP/MCPSpecificToolTests.swift b/Core/PeekabooCore/Tests/PeekabooTests/MCP/MCPSpecificToolTests.swift index 1c90015a7..36eb0f8c8 100644 --- a/Core/PeekabooCore/Tests/PeekabooTests/MCP/MCPSpecificToolTests.swift +++ b/Core/PeekabooCore/Tests/PeekabooTests/MCP/MCPSpecificToolTests.swift @@ -429,6 +429,42 @@ struct MCPSpecificToolTests { try MCPAgentTool.validatedMaxSteps(101) } } + + // MARK: - Shell Tool Tests + + @Test(.timeLimit(.minutes(1))) + func `Shell tool does not deadlock on large output`() async throws { + let tool = makeTestTool(ShellTool.init) + + // Generate output larger than the macOS pipe buffer (~64 KB) + // to trigger a deadlock if waitUntilExit() precedes pipe reads. + let result = try await tool.execute(arguments: ToolArguments(raw: [ + "command": "head -c 131072 /dev/zero | base64", + ])) + + #expect(result.isError == false) + if case let .text(text, _, _) = result.content.first { + #expect(text.count > 100_000) + } else { + Issue.record("Expected text content from shell output") + } + } + + @Test(.timeLimit(.minutes(1))) + func `Shell tool does not deadlock when both pipes exceed their buffers`() async throws { + let tool = makeTestTool(ShellTool.init) + + let result = try await tool.execute(arguments: ToolArguments(raw: [ + "command": "head -c 131072 /dev/zero >&2; head -c 131072 /dev/zero", + ])) + + #expect(result.isError == false) + if case let .text(text, _, _) = result.content.first { + #expect(text.count == 131_072) + } else { + Issue.record("Expected text content from shell output") + } + } } @MainActor