Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES = false
*/
export const DEFAULT_DIFF_FUZZY_THRESHOLD = 1.0

export const DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED = false

/**
* Terminal output preview size options for persisted command output.
*
Expand Down Expand Up @@ -136,6 +138,7 @@ export const globalSettingsSchema = z.object({
alwaysAllowModeSwitch: z.boolean().optional(),
alwaysAllowSubtasks: z.boolean().optional(),
alwaysAllowExecute: z.boolean().optional(),
destructiveCommandGuardEnabled: z.boolean().optional(),
alwaysAllowFollowupQuestions: z.boolean().optional(),
followupAutoApproveTimeoutMs: z.number().optional(),
allowedCommands: z.array(z.string()).optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export type ExtensionState = Pick<
| "alwaysAllowSubtasks"
| "alwaysAllowFollowupQuestions"
| "alwaysAllowExecute"
| "destructiveCommandGuardEnabled"
| "followupAutoApproveTimeoutMs"
| "allowedCommands"
| "deniedCommands"
Expand Down
5 changes: 5 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
openRouterDefaultModelId,
DEFAULT_WRITE_DELAY_MS,
DEFAULT_DIFF_FUZZY_THRESHOLD,
DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
Expand Down Expand Up @@ -2310,6 +2311,7 @@ export class ClineProvider
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
alwaysAllowExecute,
destructiveCommandGuardEnabled,
allowedCommands,
deniedCommands,
alwaysAllowMcp,
Expand Down Expand Up @@ -2459,6 +2461,7 @@ export class ClineProvider
alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false,
destructiveCommandGuardEnabled: destructiveCommandGuardEnabled ?? DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED,
alwaysAllowMcp: alwaysAllowMcp ?? false,
alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
Expand Down Expand Up @@ -2691,6 +2694,8 @@ export class ClineProvider
alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
destructiveCommandGuardEnabled:
stateValues.destructiveCommandGuardEnabled ?? DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED,
alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
Expand Down
17 changes: 17 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,23 @@ describe("ClineProvider", () => {
expect(state).toHaveProperty("writeDelayMs")
})

test("getStateToPostToWebview returns the saved destructive command guard setting", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("destructiveCommandGuardEnabled", true)

const state = await provider.getStateToPostToWebview()

expect(state.destructiveCommandGuardEnabled).toBe(true)
})

test("getStateToPostToWebview disables destructive command guard by default", async () => {
await provider.resolveWebviewView(mockWebviewView)

const state = await provider.getStateToPostToWebview()

expect(state.destructiveCommandGuardEnabled).toBe(false)
})

test("language is set to VSCode language", async () => {
// Mock VSCode language as Spanish
;(vscode.env as any).language = "pt-BR"
Expand Down
78 changes: 78 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ vi.mock("../../../services/command/commands", () => ({
getCommands: vi.fn(),
}))

vi.mock("../../../services/destructive-command-guard", () => ({
ensureDcgInstalled: vi.fn(),
}))

vi.mock("@anthropic-ai/vertex-sdk", () => ({
AnthropicVertex: vi.fn(),
}))
Expand Down Expand Up @@ -58,6 +62,7 @@ import type { ClineProvider } from "../ClineProvider"
import { flushModels, getModels } from "../../../api/providers/fetchers/modelCache"
import { getLMStudioModels } from "../../../api/providers/fetchers/lmstudio"
import { getCommands } from "../../../services/command/commands"
import { ensureDcgInstalled } from "../../../services/destructive-command-guard"
import {
handleCreateRule,
handleDeleteRule,
Expand Down Expand Up @@ -1098,6 +1103,79 @@ describe("webviewMessageHandler - mcpEnabled", () => {
})
})

describe("webviewMessageHandler - destructiveCommandGuardEnabled", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(ensureDcgInstalled).mockResolvedValue("/mock/global/storage/dcg")
})

it("installs and persists destructive command guard when enabled", async () => {
await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { destructiveCommandGuardEnabled: true },
})

expect(ensureDcgInstalled).toHaveBeenCalledWith("/mock/global/storage")
expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("destructiveCommandGuardEnabled", true)
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
})

it("disables the setting and reports an installation failure", async () => {
vi.mocked(ensureDcgInstalled).mockRejectedValue(new Error("checksum mismatch"))

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { destructiveCommandGuardEnabled: true },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("destructiveCommandGuardEnabled", false)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"common:errors.destructive_command_guard_enable_failed",
)
})

it("disables the setting when DCG is unavailable for the current platform", async () => {
vi.mocked(ensureDcgInstalled).mockResolvedValue(undefined)

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { destructiveCommandGuardEnabled: true },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("destructiveCommandGuardEnabled", false)
expect(t).toHaveBeenCalledWith("common:errors.destructive_command_guard_enable_failed", {
error: "common:errors.destructiveCommandGuard.unavailable",
})
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"common:errors.destructive_command_guard_enable_failed",
)
})

it("reports non-Error installation failures", async () => {
vi.mocked(ensureDcgInstalled).mockRejectedValue("download unavailable")

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { destructiveCommandGuardEnabled: true },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("destructiveCommandGuardEnabled", false)
expect(t).toHaveBeenCalledWith("common:errors.destructive_command_guard_enable_failed", {
error: "download unavailable",
})
})

it("persists disabled state without trying to install", async () => {
await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { destructiveCommandGuardEnabled: false },
})

expect(ensureDcgInstalled).not.toHaveBeenCalled()
expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("destructiveCommandGuardEnabled", false)
})
})

describe("webviewMessageHandler - terminalProfile", () => {
beforeEach(() => {
vi.clearAllMocks()
Expand Down
17 changes: 17 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,23 @@ export const webviewMessageHandler = async (

case "updateSettings":
if (message.updatedSettings) {
if (message.updatedSettings.destructiveCommandGuardEnabled === true) {
try {
const { ensureDcgInstalled } = await import("../../services/destructive-command-guard")
const binaryPath = await ensureDcgInstalled(provider.context.globalStorageUri.fsPath)
if (!binaryPath) {
throw new Error(t("common:errors.destructiveCommandGuard.unavailable"))
}
} catch (error) {
message.updatedSettings.destructiveCommandGuardEnabled = false
vscode.window.showErrorMessage(
t("common:errors.destructive_command_guard_enable_failed", {
error: error instanceof Error ? error.message : String(error),
}),
)
}
}

for (const [key, value] of Object.entries(message.updatedSettings)) {
let newValue = value

Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/ca/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/de/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"url_fetch_failed": "Failed to fetch URL content: {{error}}",
"url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}",
"command_timeout": "Command execution timed out after {{seconds}} seconds",
"destructiveCommandGuard": {
"unavailable": "Destructive Command Guard is enabled but is not available for this platform"
},
"destructive_command_guard_enable_failed": "Unable to enable Destructive Command Guard: {{error}}",
"share_task_failed": "Failed to share task. Please try again.",
"share_no_active_task": "No active task to share",
"share_auth_required": "Authentication required. Please sign in to share tasks.",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/es/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/fr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/hi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/id/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/it/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/ja/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/ko/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/nl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/pl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/pt-BR/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/i18n/locales/ru/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading