Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist
out
out-*
node_modules
package-lock.json
coverage/
mock/

Expand Down
2 changes: 1 addition & 1 deletion packages/types/npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.35.0",
"version": "1.36.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const globalSettingsSchema = z.object({
allowedCommands: z.array(z.string()).optional(),
deniedCommands: z.array(z.string()).optional(),
commandExecutionTimeout: z.number().optional(),
commandTimeoutAllowlist: z.array(z.string()).optional(),
preventCompletionWithOpenTodos: z.boolean().optional(),
allowedMaxRequests: z.number().nullish(),
autoCondenseContext: z.boolean().optional(),
Expand Down Expand Up @@ -203,6 +204,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
followupAutoApproveTimeoutMs: 0,
allowedCommands: ["*"],
commandExecutionTimeout: 30_000,
commandTimeoutAllowlist: [],
preventCompletionWithOpenTodos: false,

browserToolEnabled: false,
Expand Down
225 changes: 224 additions & 1 deletion src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import * as vscode from "vscode"
import * as fs from "fs/promises"
import { executeCommand, ExecuteCommandOptions } from "../executeCommandTool"
import { executeCommand, executeCommandTool, ExecuteCommandOptions } from "../executeCommandTool"
import { Task } from "../../task/Task"
import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"

Expand All @@ -17,6 +17,20 @@ vitest.mock("vscode", () => ({
vitest.mock("fs/promises")
vitest.mock("../../../integrations/terminal/TerminalRegistry")
vitest.mock("../../task/Task")
vitest.mock("../../prompts/responses", () => ({
formatResponse: {
toolError: vitest.fn((msg) => `Tool Error: ${msg}`),
rooIgnoreError: vitest.fn((msg) => `RooIgnore Error: ${msg}`),
},
}))
vitest.mock("../../../utils/text-normalization", () => ({
unescapeHtmlEntities: vitest.fn((text) => text),
}))
vitest.mock("../../../shared/package", () => ({
Package: {
name: "roo-cline",
},
}))

describe("Command Execution Timeout Integration", () => {
let mockTask: any
Expand Down Expand Up @@ -186,4 +200,213 @@ describe("Command Execution Timeout Integration", () => {
expect(result[0]).toBe(false) // Not rejected
expect(result[1]).not.toContain("terminated after exceeding")
})

describe("Command Timeout Allowlist", () => {
let mockBlock: any
let mockAskApproval: any
let mockHandleError: any
let mockPushToolResult: any
let mockRemoveClosingTag: any

beforeEach(() => {
// Reset mocks for allowlist tests
vitest.clearAllMocks()
;(fs.access as any).mockResolvedValue(undefined)
;(TerminalRegistry.getOrCreateTerminal as any).mockResolvedValue(mockTerminal)

// Mock the executeCommandTool parameters
mockBlock = {
params: {
command: "",
cwd: undefined,
},
partial: false,
}

mockAskApproval = vitest.fn().mockResolvedValue(true) // Always approve
mockHandleError = vitest.fn()
mockPushToolResult = vitest.fn()
mockRemoveClosingTag = vitest.fn()

// Mock task with additional properties needed by executeCommandTool
mockTask = {
cwd: "/test/directory",
terminalProcess: undefined,
providerRef: {
deref: vitest.fn().mockResolvedValue({
postMessageToWebview: vitest.fn(),
getState: vitest.fn().mockResolvedValue({
terminalOutputLineLimit: 500,
terminalShellIntegrationDisabled: false,
}),
}),
},
say: vitest.fn().mockResolvedValue(undefined),
consecutiveMistakeCount: 0,
recordToolError: vitest.fn(),
sayAndCreateMissingParamError: vitest.fn(),
rooIgnoreController: {
validateCommand: vitest.fn().mockReturnValue(null),
},
lastMessageTs: Date.now(),
ask: vitest.fn(),
didRejectTool: false,
}
})

it("should skip timeout for commands in allowlist", async () => {
// Mock VSCode configuration with timeout and allowlist
const mockGetConfiguration = vitest.fn().mockReturnValue({
get: vitest.fn().mockImplementation((key: string) => {
if (key === "commandExecutionTimeout") return 1 // 1 second timeout
if (key === "commandTimeoutAllowlist") return ["npm", "git"]
return undefined
}),
})
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration())

mockBlock.params.command = "npm install"

// Create a process that would timeout if not allowlisted
const longRunningProcess = new Promise((resolve) => {
setTimeout(resolve, 2000) // 2 seconds, longer than 1 second timeout
})
mockTerminal.runCommand.mockReturnValue(longRunningProcess)

await executeCommandTool(
mockTask as Task,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

// Should complete successfully without timeout because "npm" is in allowlist
expect(mockPushToolResult).toHaveBeenCalled()
const result = mockPushToolResult.mock.calls[0][0]
expect(result).not.toContain("terminated after exceeding")
}, 3000)

it("should apply timeout for commands not in allowlist", async () => {
// Mock VSCode configuration with timeout and allowlist
const mockGetConfiguration = vitest.fn().mockReturnValue({
get: vitest.fn().mockImplementation((key: string) => {
if (key === "commandExecutionTimeout") return 1 // 1 second timeout
if (key === "commandTimeoutAllowlist") return ["npm", "git"]
return undefined
}),
})
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration())

mockBlock.params.command = "sleep 10" // Not in allowlist

// Create a process that never resolves
const neverResolvingProcess = new Promise(() => {})
;(neverResolvingProcess as any).abort = vitest.fn()
mockTerminal.runCommand.mockReturnValue(neverResolvingProcess)

await executeCommandTool(
mockTask as Task,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

// Should timeout because "sleep" is not in allowlist
expect(mockPushToolResult).toHaveBeenCalled()
const result = mockPushToolResult.mock.calls[0][0]
expect(result).toContain("terminated after exceeding")
}, 3000)

it("should handle empty allowlist", async () => {
// Mock VSCode configuration with timeout and empty allowlist
const mockGetConfiguration = vitest.fn().mockReturnValue({
get: vitest.fn().mockImplementation((key: string) => {
if (key === "commandExecutionTimeout") return 1 // 1 second timeout
if (key === "commandTimeoutAllowlist") return []
return undefined
}),
})
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration())

mockBlock.params.command = "npm install"

// Create a process that never resolves
const neverResolvingProcess = new Promise(() => {})
;(neverResolvingProcess as any).abort = vitest.fn()
mockTerminal.runCommand.mockReturnValue(neverResolvingProcess)

await executeCommandTool(
mockTask as Task,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

// Should timeout because allowlist is empty
expect(mockPushToolResult).toHaveBeenCalled()
const result = mockPushToolResult.mock.calls[0][0]
expect(result).toContain("terminated after exceeding")
}, 3000)

it("should match command prefixes correctly", async () => {
// Mock VSCode configuration with timeout and allowlist
const mockGetConfiguration = vitest.fn().mockReturnValue({
get: vitest.fn().mockImplementation((key: string) => {
if (key === "commandExecutionTimeout") return 1 // 1 second timeout
if (key === "commandTimeoutAllowlist") return ["git log", "npm run"]
return undefined
}),
})
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration())

const longRunningProcess = new Promise((resolve) => {
setTimeout(resolve, 2000) // 2 seconds
})
const neverResolvingProcess = new Promise(() => {})
;(neverResolvingProcess as any).abort = vitest.fn()

// Test exact prefix match - should not timeout
mockBlock.params.command = "git log --oneline"
mockTerminal.runCommand.mockReturnValueOnce(longRunningProcess)

await executeCommandTool(
mockTask as Task,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

expect(mockPushToolResult).toHaveBeenCalled()
const result1 = mockPushToolResult.mock.calls[0][0]
expect(result1).not.toContain("terminated after exceeding")

// Reset mocks for second test
mockPushToolResult.mockClear()

// Test partial prefix match (should not match) - should timeout
mockBlock.params.command = "git status" // "git" alone is not in allowlist, only "git log"
mockTerminal.runCommand.mockReturnValueOnce(neverResolvingProcess)

await executeCommandTool(
mockTask as Task,
mockBlock,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

expect(mockPushToolResult).toHaveBeenCalled()
const result2 = mockPushToolResult.mock.calls[0][0]
expect(result2).toContain("terminated after exceeding")
}, 5000)
})
})
12 changes: 10 additions & 2 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,16 @@ export async function executeCommandTool(
.getConfiguration(Package.name)
.get<number>("commandExecutionTimeout", 0)

// Convert seconds to milliseconds for internal use
const commandExecutionTimeout = commandExecutionTimeoutSeconds * 1000
// Get command timeout allowlist from VSCode configuration
const commandTimeoutAllowlist = vscode.workspace
.getConfiguration(Package.name)
.get<string[]>("commandTimeoutAllowlist", [])

// Check if command matches any prefix in the allowlist
const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) => command!.startsWith(prefix.trim()))

// Convert seconds to milliseconds for internal use, but skip timeout if command is allowlisted
const commandExecutionTimeout = isCommandAllowlisted ? 0 : commandExecutionTimeoutSeconds * 1000

const options: ExecuteCommandOptions = {
executionId,
Expand Down
8 changes: 8 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@
"maximum": 600,
"description": "%commands.commandExecutionTimeout.description%"
},
"roo-cline.commandTimeoutAllowlist": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "%commands.commandTimeoutAllowlist.description%"
},
"roo-cline.preventCompletionWithOpenTodos": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Ordres que es poden executar automàticament quan 'Aprova sempre les operacions d'execució' està activat",
"commands.deniedCommands.description": "Prefixos d'ordres que seran automàticament denegats sense demanar aprovació. En cas de conflictes amb ordres permeses, la coincidència de prefix més llarga té prioritat. Afegeix * per denegar totes les ordres.",
"commands.commandExecutionTimeout.description": "Temps màxim en segons per esperar que l'execució de l'ordre es completi abans d'esgotar el temps (0 = sense temps límit, 1-600s, per defecte: 0s)",
"commands.commandTimeoutAllowlist.description": "Prefixos d'ordres que estan exclosos del temps límit d'execució d'ordres. Les ordres que coincideixin amb aquests prefixos s'executaran sense restriccions de temps límit.",
"settings.vsCodeLmModelSelector.description": "Configuració per a l'API del model de llenguatge VSCode",
"settings.vsCodeLmModelSelector.vendor.description": "El proveïdor del model de llenguatge (p. ex. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Befehle, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist",
"commands.deniedCommands.description": "Befehlspräfixe, die automatisch abgelehnt werden, ohne nach Genehmigung zu fragen. Bei Konflikten mit erlaubten Befehlen hat die längste Präfix-Übereinstimmung Vorrang. Füge * hinzu, um alle Befehle abzulehnen.",
"commands.commandExecutionTimeout.description": "Maximale Zeit in Sekunden, die auf den Abschluss der Befehlsausführung gewartet wird, bevor ein Timeout auftritt (0 = kein Timeout, 1-600s, Standard: 0s)",
"commands.commandTimeoutAllowlist.description": "Befehlspräfixe, die vom Timeout der Befehlsausführung ausgeschlossen sind. Befehle, die diesen Präfixen entsprechen, werden ohne Timeout-Beschränkungen ausgeführt.",
"settings.vsCodeLmModelSelector.description": "Einstellungen für die VSCode-Sprachmodell-API",
"settings.vsCodeLmModelSelector.vendor.description": "Der Anbieter des Sprachmodells (z.B. copilot)",
"settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.es.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Comandos que pueden ejecutarse automáticamente cuando 'Aprobar siempre operaciones de ejecución' está activado",
"commands.deniedCommands.description": "Prefijos de comandos que serán automáticamente denegados sin solicitar aprobación. En caso de conflictos con comandos permitidos, la coincidencia de prefijo más larga tiene prioridad. Añade * para denegar todos los comandos.",
"commands.commandExecutionTimeout.description": "Tiempo máximo en segundos para esperar que se complete la ejecución del comando antes de que expire (0 = sin tiempo límite, 1-600s, predeterminado: 0s)",
"commands.commandTimeoutAllowlist.description": "Prefijos de comandos que están excluidos del tiempo límite de ejecución de comandos. Los comandos que coincidan con estos prefijos se ejecutarán sin restricciones de tiempo límite.",
"settings.vsCodeLmModelSelector.description": "Configuración para la API del modelo de lenguaje VSCode",
"settings.vsCodeLmModelSelector.vendor.description": "El proveedor del modelo de lenguaje (ej. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Commandes pouvant être exécutées automatiquement lorsque 'Toujours approuver les opérations d'exécution' est activé",
"commands.deniedCommands.description": "Préfixes de commandes qui seront automatiquement refusés sans demander d'approbation. En cas de conflit avec les commandes autorisées, la correspondance de préfixe la plus longue a la priorité. Ajouter * pour refuser toutes les commandes.",
"commands.commandExecutionTimeout.description": "Temps maximum en secondes pour attendre que l'exécution de la commande se termine avant expiration (0 = pas de délai, 1-600s, défaut : 0s)",
"commands.commandTimeoutAllowlist.description": "Préfixes de commandes qui sont exclus du délai d'exécution des commandes. Les commandes correspondant à ces préfixes s'exécuteront sans restrictions de délai.",
"settings.vsCodeLmModelSelector.description": "Paramètres pour l'API du modèle de langage VSCode",
"settings.vsCodeLmModelSelector.vendor.description": "Le fournisseur du modèle de langage (ex: copilot)",
"settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो",
"commands.deniedCommands.description": "कमांड प्रीफिक्स जो स्वचालित रूप से अस्वीकार कर दिए जाएंगे बिना अनुमोदन मांगे। अनुमतित कमांड के साथ संघर्ष की स्थिति में, सबसे लंबा प्रीफिक्स मैच प्राथमिकता लेता है। सभी कमांड को अस्वीकार करने के लिए * जोड़ें।",
"commands.commandExecutionTimeout.description": "कमांड निष्पादन पूरा होने का इंतजार करने के लिए अधिकतम समय सेकंड में, समय समाप्त होने से पहले (0 = कोई समय सीमा नहीं, 1-600s, डिफ़ॉल्ट: 0s)",
"commands.commandTimeoutAllowlist.description": "कमांड प्रीफिक्स जो कमांड निष्पादन टाइमआउट से बाहर रखे गए हैं। इन प्रीफिक्स से मेल खाने वाले कमांड बिना टाइमआउट प्रतिबंधों के चलेंगे।",
"settings.vsCodeLmModelSelector.description": "VSCode भाषा मॉडल API के लिए सेटिंग्स",
"settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)",
"settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.id.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan",
"commands.deniedCommands.description": "Awalan perintah yang akan otomatis ditolak tanpa meminta persetujuan. Jika terjadi konflik dengan perintah yang diizinkan, pencocokan awalan terpanjang akan diprioritaskan. Tambahkan * untuk menolak semua perintah.",
"commands.commandExecutionTimeout.description": "Waktu maksimum dalam detik untuk menunggu eksekusi perintah selesai sebelum timeout (0 = tanpa timeout, 1-600s, default: 0s)",
"commands.commandTimeoutAllowlist.description": "Awalan perintah yang dikecualikan dari timeout eksekusi perintah. Perintah yang cocok dengan awalan ini akan berjalan tanpa batasan timeout.",
"settings.vsCodeLmModelSelector.description": "Pengaturan untuk API Model Bahasa VSCode",
"settings.vsCodeLmModelSelector.vendor.description": "Vendor dari model bahasa (misalnya copilot)",
"settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.it.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"commands.allowedCommands.description": "Comandi che possono essere eseguiti automaticamente quando 'Approva sempre le operazioni di esecuzione' è attivato",
"commands.deniedCommands.description": "Prefissi di comandi che verranno automaticamente rifiutati senza richiedere approvazione. In caso di conflitti con comandi consentiti, la corrispondenza del prefisso più lungo ha la precedenza. Aggiungi * per rifiutare tutti i comandi.",
"commands.commandExecutionTimeout.description": "Tempo massimo in secondi per attendere il completamento dell'esecuzione del comando prima del timeout (0 = nessun timeout, 1-600s, predefinito: 0s)",
"commands.commandTimeoutAllowlist.description": "Prefissi di comandi che sono esclusi dal timeout di esecuzione dei comandi. I comandi che corrispondono a questi prefissi verranno eseguiti senza restrizioni di timeout.",
"settings.vsCodeLmModelSelector.description": "Impostazioni per l'API del modello linguistico VSCode",
"settings.vsCodeLmModelSelector.vendor.description": "Il fornitore del modello linguistico (es. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)",
Expand Down
Loading