diff --git a/src/core/prompts/sections/__tests__/system-info.spec.ts b/src/core/prompts/sections/__tests__/system-info.spec.ts index 7c3b53c426..421c3a23b6 100644 --- a/src/core/prompts/sections/__tests__/system-info.spec.ts +++ b/src/core/prompts/sections/__tests__/system-info.spec.ts @@ -10,6 +10,7 @@ vi.mock("../../../../utils/shell", () => ({ })) import { getSystemInfoSection } from "../system-info" +import { getShell } from "../../../../utils/shell" import osName from "os-name" const mockOsName = osName as unknown as ReturnType @@ -101,4 +102,27 @@ describe("getSystemInfoSection", () => { // sentinel string) breaks this exact join. expect(result).toContain("default directory for all tool operations. When the user initially gives you a task") }) + + describe("shell syntax", () => { + const cmdPath = "C:\\Windows\\System32\\cmd.exe" + + it("states the dialect for cmd.exe when execute_command is available", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain(`Default Shell: ${cmdPath} +Shell Syntax: Commands run through Windows cmd.exe`) + }) + + it("omits the dialect without execute_command", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + expect(getSystemInfoSection(mockCwd, policyFor(false))).not.toContain("Shell Syntax") + }) + + it("omits the dialect for POSIX shells", () => { + expect(getSystemInfoSection(mockCwd, policyFor())).not.toContain("Shell Syntax") + }) + }) }) diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 003c555d75..6e7067bf7c 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -2,6 +2,7 @@ import os from "os" import osName from "os-name" import { getShell } from "../../../utils/shell" +import { describeShellDialect } from "../../../utils/shellDialect" import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" @@ -33,12 +34,19 @@ export function getSystemInfoSection(cwd: string, policy: EffectiveToolPolicy): ? " New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory." : "" + const shell = getShell() + const shellDialect = executeCommandAvailable ? describeShellDialect(shell) : undefined + const shellDialectLine = shellDialect + ? ` +Shell Syntax: ${shellDialect}` + : "" + const details = `==== SYSTEM INFORMATION Operating System: ${osInfo} -Default Shell: ${getShell()} +Default Shell: ${shell}${shellDialectLine} Home Directory: ${os.homedir().toPosix()} Current Workspace Directory: ${cwd.toPosix()} diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index 9855e6e417..28d5574bc0 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -31,6 +31,8 @@ import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor import { Package } from "../../shared/package" import { t } from "../../i18n" import { getTaskDirectoryPath } from "../../utils/storage" +import { getShell } from "../../utils/shell" +import { describeShellDialect, getShellName } from "../../utils/shellDialect" import { BaseTool, ToolCallbacks } from "./BaseTool" export { ShellIntegrationError } from "../../integrations/terminal/types" @@ -55,20 +57,35 @@ interface ExecuteCommandParams { timeout?: number | null } -export function formatDcgBlockedMessage(reason?: string, ruleId?: string): string { - if (reason && ruleId) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId }) - } +/** + * @param shellPath The shell the command will run in. DCG parses every command as POSIX, + * so for cmd.exe/PowerShell the message says so, letting the user tell "dangerous" from "unreadable". + */ +export function formatDcgBlockedMessage(reason?: string, ruleId?: string, shellPath?: string): string { + const message = (() => { + if (reason && ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId }) + } - if (reason) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason }) - } + if (reason) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason }) + } + + if (ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId }) + } - if (ruleId) { - return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId }) + return t("tools:executeCommand.destructiveCommandGuard.blocked") + })() + + if (shellPath && describeShellDialect(shellPath)) { + const note = t("tools:executeCommand.destructiveCommandGuard.assumedPosixNote", { + shell: getShellName(shellPath), + }) + return `${message} ${note}` } - return t("tools:executeCommand.destructiveCommandGuard.blocked") + return message } export function resolveAgentTimeoutMs(timeoutSeconds: number | null | undefined): number { @@ -182,7 +199,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { // retryable tool_error via the outer catch — never as a policy denial. dcgDecision = await runDcg(binaryPath, canonicalCommand, workingDirectory) if (dcgDecision.decision === "deny") { - await task.say("error", formatDcgBlockedMessage(dcgDecision.reason, dcgDecision.ruleId)) + await task.say("error", formatDcgBlockedMessage(dcgDecision.reason, dcgDecision.ruleId, getShell())) } } diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index c580b55102..563afef3ed 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -71,6 +71,9 @@ vitest.mock("../../../services/destructive-command-guard", () => ({ // Import the module import * as executeCommandModule from "../ExecuteCommandTool" + +// The DCG block message names the executing shell; pin it so tests do not depend on the host. +vi.mock("../../../utils/shell", () => ({ getShell: vi.fn(() => "/bin/bash") })) const { executeCommandTool } = executeCommandModule // Spies installed on the auto-approval module are not restored between tests, @@ -316,6 +319,17 @@ describe("executeCommandTool", () => { expect(executeCommandModule.formatDcgBlockedMessage(reason, ruleId)).toBe(expected) }) + it.each([ + ["C:\\Windows\\System32\\cmd.exe", true], + ["C:\\Program Files\\PowerShell\\7\\pwsh.exe", true], + ["/bin/bash", false], + [undefined, false], + ])("appends the POSIX-assumption note only for non-POSIX shell %s", (shellPath, expectNote) => { + const message = executeCommandModule.formatDcgBlockedMessage(undefined, undefined, shellPath) + + expect(message.includes("executeCommand.destructiveCommandGuard.assumedPosixNote")).toBe(expectNote) + }) + it("shows a DCG block message as an error before requesting explicit approval", async () => { const provider = await mockCline.providerRef.deref() provider.context = { globalStorageUri: { fsPath: "/test/storage" } } diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 6e716c4955..f2a1b24a34 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ha bloquejat aquesta ordre.", "blockedWithReason": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard ha bloquejat aquesta ordre. (Regla: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}} (Regla: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ha bloquejat aquesta ordre. Missatge de DCG: {{reason}} (Regla: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analitza les ordres com a sintaxi d'intèrpret POSIX, però aquesta ordre s'executa a {{shell}}, de manera que un bloqueig pot voler dir que DCG no ha pogut llegir l'ordre i no que sigui destructiva." } }, "newTask": { diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 695f0b444e..6e40fd6ade 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard hat diesen Befehl blockiert.", "blockedWithReason": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard hat diesen Befehl blockiert. (Regel: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}} (Regel: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard hat diesen Befehl blockiert. Nachricht von DCG: {{reason}} (Regel: {{ruleId}})", + "assumedPosixNote": "Hinweis: DCG analysiert Befehle als POSIX-Shell-Syntax, dieser Befehl läuft jedoch in {{shell}}. Eine Blockierung kann daher bedeuten, dass DCG den Befehl nicht lesen konnte, und nicht, dass er destruktiv ist." } }, "newTask": { diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 306e71ac86..602b036b72 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard blocked this command.", "blockedWithReason": "Destructive Command Guard blocked this command. Message from DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard blocked this command. (Rule: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})", + "assumedPosixNote": "Note: DCG analyzes commands as POSIX shell syntax, but this command runs in {{shell}}, so a block may mean DCG could not read the command rather than that it is destructive." } }, "newTask": { diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 07cc77d154..e510566530 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard bloqueó este comando.", "blockedWithReason": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard bloqueó este comando. (Regla: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}} (Regla: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard bloqueó este comando. Mensaje de DCG: {{reason}} (Regla: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analiza los comandos como sintaxis de shell POSIX, pero este comando se ejecuta en {{shell}}, por lo que un bloqueo puede significar que DCG no pudo leer el comando y no que sea destructivo." } }, "newTask": { diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index d5d09dce47..a2657d403e 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard a bloqué cette commande.", "blockedWithReason": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}}", "blockedWithRule": "Destructive Command Guard a bloqué cette commande. (Règle : {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}} (Règle : {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard a bloqué cette commande. Message de DCG : {{reason}} (Règle : {{ruleId}})", + "assumedPosixNote": "Remarque : DCG analyse les commandes comme de la syntaxe shell POSIX, mais cette commande s'exécute dans {{shell}} ; un blocage peut donc signifier que DCG n'a pas pu lire la commande, et non qu'elle est destructrice." } }, "newTask": { diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 69cf79e6d2..59eb172699 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया।", "blockedWithReason": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}}", "blockedWithRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। (नियम: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}} (नियम: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}} (नियम: {{ruleId}})", + "assumedPosixNote": "नोट: DCG कमांड को POSIX शेल सिंटैक्स के रूप में विश्लेषित करता है, लेकिन यह कमांड {{shell}} में चलती है, इसलिए ब्लॉक का मतलब यह हो सकता है कि DCG कमांड को पढ़ नहीं सका, न कि वह विनाशकारी है।" } }, "newTask": { diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 0f7c9b7fdf..20eeb9ce77 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard memblokir perintah ini.", "blockedWithReason": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard memblokir perintah ini. (Aturan: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}} (Aturan: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard memblokir perintah ini. Pesan dari DCG: {{reason}} (Aturan: {{ruleId}})", + "assumedPosixNote": "Catatan: DCG menganalisis perintah sebagai sintaks shell POSIX, tetapi perintah ini berjalan di {{shell}}, sehingga pemblokiran bisa berarti DCG tidak dapat membaca perintah, bukan bahwa perintah tersebut destruktif." } }, "searchFiles": { diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index b0051abf59..8fce341099 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard ha bloccato questo comando.", "blockedWithReason": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard ha bloccato questo comando. (Regola: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}} (Regola: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard ha bloccato questo comando. Messaggio da DCG: {{reason}} (Regola: {{ruleId}})", + "assumedPosixNote": "Nota: DCG analizza i comandi come sintassi shell POSIX, ma questo comando viene eseguito in {{shell}}, quindi un blocco può significare che DCG non è riuscito a leggere il comando, non che sia distruttivo." } }, "newTask": { diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index fa404f7576..c3dc030536 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard がこのコマンドをブロックしました。", "blockedWithReason": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}", "blockedWithRule": "Destructive Command Guard がこのコマンドをブロックしました。(ルール: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}(ルール: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}(ルール: {{ruleId}})", + "assumedPosixNote": "注: DCG はコマンドを POSIX シェル構文として解析しますが、このコマンドは {{shell}} で実行されます。ブロックは、コマンドが破壊的であるためではなく、DCG がコマンドを読み取れなかったことを意味する場合があります。" } }, "newTask": { diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 39335eb6e4..d43ecf0573 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard가 이 명령어를 차단했습니다.", "blockedWithReason": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}}", "blockedWithRule": "Destructive Command Guard가 이 명령어를 차단했습니다. (규칙: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}} (규칙: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}} (규칙: {{ruleId}})", + "assumedPosixNote": "참고: DCG는 명령을 POSIX 셸 구문으로 분석하지만 이 명령은 {{shell}}에서 실행됩니다. 따라서 차단은 명령이 파괴적이라는 뜻이 아니라 DCG가 명령을 읽지 못했다는 뜻일 수 있습니다." } }, "newTask": { diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index d68de44d80..329e052b88 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard heeft deze opdracht geblokkeerd.", "blockedWithReason": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. (Regel: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}} (Regel: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard heeft deze opdracht geblokkeerd. Bericht van DCG: {{reason}} (Regel: {{ruleId}})", + "assumedPosixNote": "Opmerking: DCG analyseert opdrachten als POSIX-shellsyntaxis, maar deze opdracht wordt uitgevoerd in {{shell}}. Een blokkade kan dus betekenen dat DCG de opdracht niet kon lezen, niet dat deze destructief is." } }, "newTask": { diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index f7876025fc..e1f2c4777c 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard zablokował to polecenie.", "blockedWithReason": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard zablokował to polecenie. (Reguła: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}} (Reguła: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard zablokował to polecenie. Komunikat od DCG: {{reason}} (Reguła: {{ruleId}})", + "assumedPosixNote": "Uwaga: DCG analizuje polecenia jako składnię powłoki POSIX, ale to polecenie jest uruchamiane w {{shell}}, więc blokada może oznaczać, że DCG nie mógł odczytać polecenia, a nie że jest ono destrukcyjne." } }, "newTask": { diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 3b423d3784..8e63cbff7a 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -19,7 +19,8 @@ "blocked": "O Destructive Command Guard bloqueou este comando.", "blockedWithReason": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}}", "blockedWithRule": "O Destructive Command Guard bloqueou este comando. (Regra: {{ruleId}})", - "blockedWithReasonAndRule": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}} (Regra: {{ruleId}})" + "blockedWithReasonAndRule": "O Destructive Command Guard bloqueou este comando. Mensagem do DCG: {{reason}} (Regra: {{ruleId}})", + "assumedPosixNote": "Observação: o DCG analisa comandos como sintaxe de shell POSIX, mas este comando é executado em {{shell}}, então um bloqueio pode significar que o DCG não conseguiu ler o comando, e não que ele seja destrutivo." } }, "newTask": { diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index ff8db8fe2e..c30473432d 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard заблокировал эту команду.", "blockedWithReason": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard заблокировал эту команду. (Правило: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}} (Правило: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}} (Правило: {{ruleId}})", + "assumedPosixNote": "Примечание: DCG анализирует команды как синтаксис оболочки POSIX, но эта команда выполняется в {{shell}}, поэтому блокировка может означать, что DCG не смог прочитать команду, а не то, что она разрушительна." } }, "newTask": { diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 4769a826b4..7017425595 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard bu komutu engelledi.", "blockedWithReason": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}}", "blockedWithRule": "Destructive Command Guard bu komutu engelledi. (Kural: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}} (Kural: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard bu komutu engelledi. DCG mesajı: {{reason}} (Kural: {{ruleId}})", + "assumedPosixNote": "Not: DCG komutları POSIX kabuk sözdizimi olarak analiz eder, ancak bu komut {{shell}} içinde çalışır; bu nedenle bir engelleme, komutun yıkıcı olduğunu değil DCG'nin komutu okuyamadığını gösteriyor olabilir." } }, "newTask": { diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index d50dd50c65..0b546060a5 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard đã chặn lệnh này.", "blockedWithReason": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}}", "blockedWithRule": "Destructive Command Guard đã chặn lệnh này. (Quy tắc: {{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}} (Quy tắc: {{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard đã chặn lệnh này. Thông báo từ DCG: {{reason}} (Quy tắc: {{ruleId}})", + "assumedPosixNote": "Lưu ý: DCG phân tích lệnh theo cú pháp shell POSIX, nhưng lệnh này chạy trong {{shell}}, vì vậy việc chặn có thể nghĩa là DCG không đọc được lệnh chứ không phải lệnh đó mang tính phá hủy." } }, "newTask": { diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 2af580bb6a..8bf46565de 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard 已拦截此命令。", "blockedWithReason": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}", "blockedWithRule": "Destructive Command Guard 已拦截此命令。(规则:{{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}(规则:{{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}(规则:{{ruleId}})", + "assumedPosixNote": "注意:DCG 按 POSIX shell 语法分析命令,但此命令在 {{shell}} 中运行,因此拦截可能意味着 DCG 无法解析该命令,而不是该命令具有破坏性。" } }, "newTask": { diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 75c8047945..b838b39677 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -19,7 +19,8 @@ "blocked": "Destructive Command Guard 已阻擋此命令。", "blockedWithReason": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}", "blockedWithRule": "Destructive Command Guard 已阻擋此命令。(規則:{{ruleId}})", - "blockedWithReasonAndRule": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}(規則:{{ruleId}})" + "blockedWithReasonAndRule": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}(規則:{{ruleId}})", + "assumedPosixNote": "注意:DCG 以 POSIX shell 語法分析指令,但此指令在 {{shell}} 中執行,因此封鎖可能代表 DCG 無法解析該指令,而不是該指令具有破壞性。" } }, "newTask": { diff --git a/src/services/destructive-command-guard/__tests__/runner.spec.ts b/src/services/destructive-command-guard/__tests__/runner.spec.ts index ddf991f5f6..3a023229dc 100644 --- a/src/services/destructive-command-guard/__tests__/runner.spec.ts +++ b/src/services/destructive-command-guard/__tests__/runner.spec.ts @@ -4,7 +4,7 @@ import { PassThrough } from "stream" import { spawn } from "child_process" import { DCG_MAX_OUTPUT_BYTES } from "../constants" -import { runDcg } from "../runner" +import { getDcgArgs, runDcg } from "../runner" vi.mock("child_process", () => ({ spawn: vi.fn() })) @@ -167,3 +167,21 @@ describe("runDcg", () => { expect(child.kill).toHaveBeenCalledWith("SIGKILL") }) }) + +describe("getDcgArgs", () => { + it("enables the Windows packs on win32", () => { + expect(getDcgArgs("rd /s /q foo", "win32")).toEqual([ + "test", + "--format", + "json", + "--no-color", + "--with-packs", + "windows.filesystem,windows.misc", + "rd /s /q foo", + ]) + }) + + it.each(["linux", "darwin"] as const)("passes no pack options on %s", (platform) => { + expect(getDcgArgs("ls", platform)).toEqual(["test", "--format", "json", "--no-color", "ls"]) + }) +}) diff --git a/src/services/destructive-command-guard/runner.ts b/src/services/destructive-command-guard/runner.ts index 6dd8d4663b..b5830f8887 100644 --- a/src/services/destructive-command-guard/runner.ts +++ b/src/services/destructive-command-guard/runner.ts @@ -23,9 +23,17 @@ function getDcgEnvironment(): NodeJS.ProcessEnv { return env } +// These packs are off by default; they cover cmd.exe forms such as `rd /s /q` and `format`. +const DCG_WINDOWS_PACKS = "windows.filesystem,windows.misc" + +export function getDcgArgs(command: string, platform: NodeJS.Platform = process.platform): string[] { + const packArgs = platform === "win32" ? ["--with-packs", DCG_WINDOWS_PACKS] : [] + return ["test", "--format", "json", "--no-color", ...packArgs, command] +} + export function runDcg(binaryPath: string, command: string, cwd: string): Promise { return new Promise((resolve, reject) => { - const child = spawn(binaryPath, ["test", "--format", "json", "--no-color", command], { + const child = spawn(binaryPath, getDcgArgs(command), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts new file mode 100644 index 0000000000..c5db9ee722 --- /dev/null +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -0,0 +1,23 @@ +import { describeShellDialect } from "../shellDialect" + +describe("describeShellDialect", () => { + it.each([ + { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe" }, + { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe" }, + { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell" }, + { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell" }, + ])("names the dialect for $shell", ({ shell, expected }) => { + expect(describeShellDialect(shell)).toContain(expected) + }) + + it("warns that ';' does not chain commands in cmd.exe", () => { + expect(describeShellDialect("C:\\Windows\\System32\\cmd.exe")).toContain("`;` is not a separator") + }) + + it.each(["/bin/bash", "/bin/zsh", "C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Windows\\System32\\wsl.exe"])( + "adds nothing for POSIX shell %s", + (shell) => { + expect(describeShellDialect(shell)).toBeUndefined() + }, + ) +}) diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts new file mode 100644 index 0000000000..31f65ce2b7 --- /dev/null +++ b/src/utils/shellDialect.ts @@ -0,0 +1,34 @@ +/** + * Describes the command syntax of Windows-native shells for the system prompt. + * + * Rules and habits written for bash silently do the wrong thing under these shells + * (e.g. `;` is not a command separator in cmd.exe), so the model is told which + * dialect it is writing for. Returns undefined for POSIX shells, where the shell + * path already names the dialect. + */ +export function getShellName(shellPath: string): string { + // path.basename follows the host platform; split on both separators so Windows paths work anywhere. + return shellPath.split(/[\\/]/).pop() ?? "" +} + +export function describeShellDialect(shellPath: string): string | undefined { + const name = getShellName(shellPath).toLowerCase() + + if (name === "cmd.exe" || name === "cmd") { + return ( + "Commands run through Windows cmd.exe, not bash. Use cmd syntax: `&` or `&&` to chain commands " + + "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, `%ERRORLEVEL%` for exit codes, " + + "and `dir`, `type`, `findstr`. POSIX tools may be missing or shadowed by other binaries on PATH." + ) + } + + if (name === "powershell.exe" || name === "pwsh.exe" || name === "powershell" || name === "pwsh") { + return ( + "Commands run through PowerShell, not bash. Use PowerShell syntax: `;` to chain commands, " + + "`$env:VAR` for environment variables, `$LASTEXITCODE` for exit codes, and cmdlets such as " + + "`Get-ChildItem` and `Select-String`." + ) + } + + return undefined +}