fix: prevent queued messages from auto-approving unapproved commands

When a queued message was drained during a tool/command/mcp ask, the code
unconditionally sent "yesButtonClicked", which approved the pending
command regardless of auto-approval settings. This changes the response
to "messageResponse" instead, matching the behavior when a user types a
reply without clicking Approve (the command gets rejected).

Fixes #12302
This commit is contained in:
Roo Code 2026-05-09 11:49:12 +00:00
parent ad25634905
commit 839b63c1e7
2 changed files with 93 additions and 6 deletions

View file

@ -1432,9 +1432,12 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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)
}

View file

@ -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