diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 005bb0f292b..7ef0cfb60eb 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1432,9 +1432,12 @@ export class Task extends EventEmitter implements TaskLike { if (message) { // Check if this is a tool approval ask that needs to be handled. if (type === "tool" || type === "command" || type === "use_mcp_server") { - // For tool approvals, we need to approve first, then send - // the message if there's text/images. - this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images) + // For tool/command approvals, reject the pending tool and + // send the queued text as a plain message -- matching the + // behaviour when a user types a reply without clicking + // Approve. This prevents queued messages from + // unconditionally auto-approving unapproved commands. + this.handleWebviewAskResponse("messageResponse", message.text, message.images) } else { // For other ask types (like followup or command_output), fulfill the ask // directly. @@ -1456,10 +1459,11 @@ export class Task extends EventEmitter implements TaskLike { if (shouldDrainQueuedMessageForAsk && !this.messageQueueService.isEmpty()) { const message = this.messageQueueService.dequeueMessage() if (message) { - // If this is a tool approval ask, we need to approve first (yesButtonClicked) - // and include any queued text/images. + // If this is a tool approval ask, reject the pending tool and + // forward the queued text as a plain message so that + // unapproved commands are never auto-executed. if (type === "tool" || type === "command" || type === "use_mcp_server") { - this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images) + this.handleWebviewAskResponse("messageResponse", message.text, message.images) } else { this.handleWebviewAskResponse("messageResponse", message.text, message.images) } diff --git a/src/core/task/__tests__/ask-queued-message-drain.spec.ts b/src/core/task/__tests__/ask-queued-message-drain.spec.ts index 06f577881ec..1a0ad177ac5 100644 --- a/src/core/task/__tests__/ask-queued-message-drain.spec.ts +++ b/src/core/task/__tests__/ask-queued-message-drain.spec.ts @@ -36,6 +36,89 @@ describe("Task.ask queued message drain", () => { expect(result.text).toBe("picked answer") }) + it("rejects unapproved command asks with messageResponse when queued message is drained", async () => { + const task = Object.create(Task.prototype) as Task + ;(task as any).abort = false + ;(task as any).clineMessages = [] + ;(task as any).askResponse = undefined + ;(task as any).askResponseText = undefined + ;(task as any).askResponseImages = undefined + ;(task as any).lastMessageTs = undefined + + const { MessageQueueService } = await import("../../message-queue/MessageQueueService") + ;(task as any).messageQueueService = new MessageQueueService() + ;(task as any).addToClineMessages = vi.fn(async () => {}) + ;(task as any).saveClineMessages = vi.fn(async () => {}) + ;(task as any).updateClineMessage = vi.fn(async () => {}) + ;(task as any).cancelAutoApprovalTimeout = vi.fn(() => {}) + ;(task as any).checkpointSave = vi.fn(async () => {}) + ;(task as any).emit = vi.fn() + ;(task as any).providerRef = { deref: () => undefined } + + // Queue a message before asking for command approval. + ;(task as any).messageQueueService.addMessage("continue please") + + const result = await task.ask("command", "rm -rf /tmp/test", false) + + // The queued message must NOT approve the command -- it should reject + // with messageResponse, matching the behaviour of typing a reply + // without clicking Approve. + expect(result.response).toBe("messageResponse") + expect(result.text).toBe("continue please") + }) + + it("rejects unapproved tool asks with messageResponse when queued message is drained", async () => { + const task = Object.create(Task.prototype) as Task + ;(task as any).abort = false + ;(task as any).clineMessages = [] + ;(task as any).askResponse = undefined + ;(task as any).askResponseText = undefined + ;(task as any).askResponseImages = undefined + ;(task as any).lastMessageTs = undefined + + const { MessageQueueService } = await import("../../message-queue/MessageQueueService") + ;(task as any).messageQueueService = new MessageQueueService() + ;(task as any).addToClineMessages = vi.fn(async () => {}) + ;(task as any).saveClineMessages = vi.fn(async () => {}) + ;(task as any).updateClineMessage = vi.fn(async () => {}) + ;(task as any).cancelAutoApprovalTimeout = vi.fn(() => {}) + ;(task as any).checkpointSave = vi.fn(async () => {}) + ;(task as any).emit = vi.fn() + ;(task as any).providerRef = { deref: () => undefined } + ;(task as any).messageQueueService.addMessage("go ahead") + + const result = await task.ask("tool", "write_to_file /etc/hosts", false) + + expect(result.response).toBe("messageResponse") + expect(result.text).toBe("go ahead") + }) + + it("rejects unapproved use_mcp_server asks with messageResponse when queued message is drained", async () => { + const task = Object.create(Task.prototype) as Task + ;(task as any).abort = false + ;(task as any).clineMessages = [] + ;(task as any).askResponse = undefined + ;(task as any).askResponseText = undefined + ;(task as any).askResponseImages = undefined + ;(task as any).lastMessageTs = undefined + + const { MessageQueueService } = await import("../../message-queue/MessageQueueService") + ;(task as any).messageQueueService = new MessageQueueService() + ;(task as any).addToClineMessages = vi.fn(async () => {}) + ;(task as any).saveClineMessages = vi.fn(async () => {}) + ;(task as any).updateClineMessage = vi.fn(async () => {}) + ;(task as any).cancelAutoApprovalTimeout = vi.fn(() => {}) + ;(task as any).checkpointSave = vi.fn(async () => {}) + ;(task as any).emit = vi.fn() + ;(task as any).providerRef = { deref: () => undefined } + ;(task as any).messageQueueService.addMessage("next step") + + const result = await task.ask("use_mcp_server", "mcp server call", false) + + expect(result.response).toBe("messageResponse") + expect(result.text).toBe("next step") + }) + it("does not consume queued messages for command_output asks", async () => { const task = Object.create(Task.prototype) as Task ;(task as any).abort = false