From 5f6e0a8ca87a2503f4b3e4feed347c263df36e1c Mon Sep 17 00:00:00 2001 From: Naved Date: Wed, 29 Jul 2026 21:18:37 -0700 Subject: [PATCH] feat: integrate destructive command guard with auto-approval Closes #1058 Completes #1049 --- AGENTS.md | 16 +++ packages/types/src/__tests__/message.test.ts | 15 ++ packages/types/src/message.ts | 1 + src/core/auto-approval/__tests__/dcg.spec.ts | 69 +++++++++ src/core/auto-approval/index.ts | 11 ++ src/core/task/Task.ts | 4 + src/core/tools/ExecuteCommandTool.ts | 52 ++++++- .../__tests__/executeCommandTool.spec.ts | 133 ++++++++++++++++++ src/i18n/locales/ca/tools.json | 8 ++ src/i18n/locales/de/tools.json | 8 ++ src/i18n/locales/en/tools.json | 8 ++ src/i18n/locales/es/tools.json | 8 ++ src/i18n/locales/fr/tools.json | 8 ++ src/i18n/locales/hi/tools.json | 8 ++ src/i18n/locales/id/tools.json | 8 ++ src/i18n/locales/it/tools.json | 8 ++ src/i18n/locales/ja/tools.json | 8 ++ src/i18n/locales/ko/tools.json | 8 ++ src/i18n/locales/nl/tools.json | 8 ++ src/i18n/locales/pl/tools.json | 8 ++ src/i18n/locales/pt-BR/tools.json | 8 ++ src/i18n/locales/ru/tools.json | 8 ++ src/i18n/locales/tr/tools.json | 8 ++ src/i18n/locales/vi/tools.json | 8 ++ src/i18n/locales/zh-CN/tools.json | 8 ++ src/i18n/locales/zh-TW/tools.json | 8 ++ webview-ui/src/components/chat/ChatRow.tsx | 14 ++ .../src/components/chat/CommandExecution.tsx | 10 +- .../__tests__/ChatRow.command-denied.spec.tsx | 70 +++++++++ .../chat/__tests__/CommandExecution.spec.tsx | 28 ++++ webview-ui/src/i18n/locales/ca/chat.json | 1 + webview-ui/src/i18n/locales/de/chat.json | 1 + webview-ui/src/i18n/locales/en/chat.json | 1 + webview-ui/src/i18n/locales/es/chat.json | 1 + webview-ui/src/i18n/locales/fr/chat.json | 1 + webview-ui/src/i18n/locales/hi/chat.json | 1 + webview-ui/src/i18n/locales/id/chat.json | 1 + webview-ui/src/i18n/locales/it/chat.json | 1 + webview-ui/src/i18n/locales/ja/chat.json | 1 + webview-ui/src/i18n/locales/ko/chat.json | 1 + webview-ui/src/i18n/locales/nl/chat.json | 1 + webview-ui/src/i18n/locales/pl/chat.json | 1 + webview-ui/src/i18n/locales/pt-BR/chat.json | 1 + webview-ui/src/i18n/locales/ru/chat.json | 1 + webview-ui/src/i18n/locales/tr/chat.json | 1 + webview-ui/src/i18n/locales/vi/chat.json | 1 + webview-ui/src/i18n/locales/zh-CN/chat.json | 1 + webview-ui/src/i18n/locales/zh-TW/chat.json | 1 + 48 files changed, 577 insertions(+), 8 deletions(-) create mode 100644 src/core/auto-approval/__tests__/dcg.spec.ts create mode 100644 webview-ui/src/components/chat/__tests__/ChatRow.command-denied.spec.tsx diff --git a/AGENTS.md b/AGENTS.md index 64085dc087..1b70c7347b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,22 @@ When writing new code: - After editing a file, run `pnpm --dir src exec eslint --prune-suppressions --max-warnings=0 ` and confirm the count for that file did not increase. - If a suppression is truly unavoidable (e.g. `vi.spyOn(Cls.prototype as any, "privateMethod")` where no typed alternative exists), document why in a comment next to the cast. +## Persisted Setting Checklist + +When adding or changing a user setting, trace the complete round trip. A setting is not complete merely because its control renders or its value reaches storage. + +- [ ] Define the setting, validation, and optionality in `packages/types/src/global-settings.ts` (or the appropriate provider/settings schema). Define a shared default constant when multiple readers need the same default. +- [ ] If the webview uses the setting, include it in `ExtensionState` in `packages/types/src/vscode-extension-host.ts` and any relevant message types. +- [ ] In `SettingsView`, initialize and read the control from local `cachedState`, NOT directly from live `useExtensionState()`. The cache buffers edits until the user explicitly clicks Save; binding to live state causes races and discarded edits. +- [ ] Update `cachedState` from the control and include the setting in the `updateSettings` payload sent by `SettingsView.handleSubmit()` (or document and test a deliberate immediate-save flow). +- [ ] Verify `webviewMessageHandler` handles any setting-specific normalization or side effects and persists the final value through `ContextProxy`. Generic settings normally use `contextProxy.setValue()`. +- [ ] Add the setting to `ClineProvider.getState()` with the intended default so extension/runtime consumers can read it. +- [ ] Add the setting to both the destructuring and returned object in `ClineProvider.getStateToPostToWebview()`. This completes the storage-to-webview round trip and prevents a saved control from reverting visually. +- [ ] Update every runtime consumer and ensure all consumers use the same default semantics. +- [ ] If users can import/export the setting, verify its schema inclusion makes it round-trip and add special handling only when required (for example, secrets or non-exportable state). +- [ ] Add focused tests for: UI binding/save behavior, persistence or normalization, and the saved value returned by `getStateToPostToWebview()`. Include both `true` and `false`/unset cases when defaulting can hide omissions. +- [ ] Run the narrowest relevant Vitest suites from the package directory that declares Vitest. + ## Test Placement Guidance Prefer the narrowest test layer that proves the behavior. This follows standard test-pyramid guidance: keep most coverage in fast, focused tests; add integration tests for cross-module contracts; reserve end-to-end tests for full workflow confidence. diff --git a/packages/types/src/__tests__/message.test.ts b/packages/types/src/__tests__/message.test.ts index b9b5fbdaea..1fad7c27ed 100644 --- a/packages/types/src/__tests__/message.test.ts +++ b/packages/types/src/__tests__/message.test.ts @@ -2,6 +2,7 @@ import { clineAsks, + clineMessageSchema, getCompletionCheckpoint, isIdleAsk, isInteractiveAsk, @@ -21,6 +22,20 @@ describe("ask messages", () => { }) }) +describe("clineMessageSchema autoApprovalDecision", () => { + it.each(["approve", "deny"] as const)("accepts %s", (autoApprovalDecision) => { + expect(clineMessageSchema.safeParse({ ts: 1, type: "ask", ask: "command", autoApprovalDecision }).success).toBe( + true, + ) + }) + + it("rejects invalid decisions", () => { + expect( + clineMessageSchema.safeParse({ ts: 1, type: "ask", ask: "command", autoApprovalDecision: "ask" }).success, + ).toBe(false) + }) +}) + describe("getCompletionCheckpoint", () => { it("returns the first checkpoint after the latest user prompt before completion", () => { const messages: ClineMessage[] = [ diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index dc0e3dfff5..28d5af82ac 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -272,6 +272,7 @@ export const clineMessageSchema = z.object({ isProtected: z.boolean().optional(), apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(), isAnswered: z.boolean().optional(), + autoApprovalDecision: z.union([z.literal("approve"), z.literal("deny")]).optional(), }) export type ClineMessage = z.infer diff --git a/src/core/auto-approval/__tests__/dcg.spec.ts b/src/core/auto-approval/__tests__/dcg.spec.ts new file mode 100644 index 0000000000..db61a5d7db --- /dev/null +++ b/src/core/auto-approval/__tests__/dcg.spec.ts @@ -0,0 +1,69 @@ +import { checkAutoApproval } from ".." + +describe("Destructive Command Guard auto-approval precedence", () => { + const baseState = { + autoApprovalEnabled: true, + alwaysAllowExecute: true, + alwaysAllowReadOnly: false, + alwaysAllowReadOnlyOutsideWorkspace: false, + alwaysAllowWrite: false, + alwaysAllowWriteOutsideWorkspace: false, + alwaysAllowWriteProtected: false, + alwaysAllowMcp: false, + alwaysAllowModeSwitch: false, + alwaysAllowSubtasks: false, + alwaysAllowFollowupQuestions: false, + allowedCommands: ["echo"], + deniedCommands: ["rm"], + destructiveCommandGuardEnabled: true, + mcpServers: [], + } + + it("auto-approves commands allowed by DCG without consulting Zoo's deny list", async () => { + expect(await checkAutoApproval({ state: baseState, ask: "command", text: "rm file" })).toEqual({ + decision: "approve", + }) + }) + + it("requires explicit approval for a DCG-protected command", async () => { + expect( + await checkAutoApproval({ state: baseState, ask: "command", text: "echo safe", isProtected: true }), + ).toEqual({ decision: "ask" }) + }) + + it("auto-approves DCG-allowed commands without consulting Zoo's allowlist", async () => { + expect(await checkAutoApproval({ state: baseState, ask: "command", text: "unlisted-command" })).toEqual({ + decision: "approve", + }) + }) + + it("does not auto-approve via DCG when execute auto-approval is off", async () => { + const state = { ...baseState, alwaysAllowExecute: false } + + expect(await checkAutoApproval({ state, ask: "command", text: "echo safe" })).toEqual({ decision: "ask" }) + }) + + it("keeps ordinary allowlist auto-approval when DCG is disabled", async () => { + const state = { ...baseState, destructiveCommandGuardEnabled: false } + + expect(await checkAutoApproval({ state, ask: "command", text: "echo safe" })).toEqual({ + decision: "approve", + }) + }) + + it("keeps ordinary denylist behavior when DCG is disabled", async () => { + const state = { ...baseState, destructiveCommandGuardEnabled: false } + + expect(await checkAutoApproval({ state, ask: "command", text: "rm file" })).toEqual({ + decision: "deny", + }) + }) + + it("keeps ordinary prompts for unlisted commands when DCG is disabled", async () => { + const state = { ...baseState, destructiveCommandGuardEnabled: false } + + expect(await checkAutoApproval({ state, ask: "command", text: "unlisted-command" })).toEqual({ + decision: "ask", + }) + }) +}) diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index c8293c2a79..09e665f141 100644 --- a/src/core/auto-approval/index.ts +++ b/src/core/auto-approval/index.ts @@ -33,6 +33,7 @@ export type AutoApprovalStateOptions = | "mcpServers" // For `alwaysAllowMcp`. | "allowedCommands" // For `alwaysAllowExecute`. | "deniedCommands" + | "destructiveCommandGuardEnabled" export type CheckAutoApprovalResult = | { decision: "approve" } @@ -115,8 +116,18 @@ export async function checkAutoApproval({ if (!text) { return { decision: "ask" } } + if (isProtected) { + return { decision: "ask" } + } if (state.alwaysAllowExecute === true) { + // Execute commands immediately when DCG allows them. ExecuteCommandTool + // marks commands blocked by DCG as protected before reaching this check, + // which keeps the explicit user approval prompt for those commands. + if (state.destructiveCommandGuardEnabled === true) { + return { decision: "approve" } + } + const decision = getCommandDecision(text, state.allowedCommands || [], state.deniedCommands || []) if (decision === "auto_approve") { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8f69a3a0d4..c5127feb17 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1185,6 +1185,7 @@ export class Task extends EventEmitter implements TaskLike { const state = provider ? await provider.getState() : undefined const approval = await checkAutoApproval({ state, ask: type, text, isProtected }) const isAutoAnswered = approval.decision === "approve" || approval.decision === "deny" + const autoApprovalDecision = isAutoAnswered ? approval.decision : undefined if (partial !== undefined) { const lastMessage = this.clineMessages.at(-1) @@ -1248,6 +1249,7 @@ export class Task extends EventEmitter implements TaskLike { lastMessage.isProtected = isProtected if (isAutoAnswered) { lastMessage.isAnswered = true + lastMessage.autoApprovalDecision = autoApprovalDecision } await this.saveClineMessages() // Fire-and-forget: see updateClineMessage call above for the @@ -1269,6 +1271,7 @@ export class Task extends EventEmitter implements TaskLike { text, isProtected, isAnswered: isAutoAnswered || undefined, + autoApprovalDecision, }) } } @@ -1286,6 +1289,7 @@ export class Task extends EventEmitter implements TaskLike { text, isProtected, isAnswered: isAutoAnswered || undefined, + autoApprovalDecision, }) } diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index 6e8a9becb4..306557dba8 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -60,6 +60,22 @@ interface ExecuteCommandParams { timeout?: number | null } +export function formatDcgBlockedMessage(reason?: string, ruleId?: string): string { + if (reason && ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId }) + } + + if (reason) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason }) + } + + if (ruleId) { + return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId }) + } + + return t("tools:executeCommand.destructiveCommandGuard.blocked") +} + export function resolveAgentTimeoutMs(timeoutSeconds: number | null | undefined): number { const requestedAgentTimeout = typeof timeoutSeconds === "number" && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 0 @@ -115,16 +131,44 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { return } - const didApprove = await askApproval("command", canonicalCommand) + const provider = await task.providerRef.deref() + const providerState = await provider?.getState() + let dcgBlocked = false + if (providerState?.destructiveCommandGuardEnabled === true) { + const { ensureDcgInstalled, runDcg } = await import("../../services/destructive-command-guard") + if (!provider) { + throw new Error(t("common:errors.destructiveCommandGuard.unavailable")) + } + // Resolve through the managed installer on use so an extension update + // automatically installs the newly pinned and verified DCG version. + const binaryPath = await ensureDcgInstalled(provider.context.globalStorageUri.fsPath) + if (!binaryPath) { + throw new Error(t("common:errors.destructiveCommandGuard.unavailable")) + } + const workingDirectory = customCwd + ? path.isAbsolute(customCwd) + ? customCwd + : path.resolve(task.cwd, customCwd) + : task.cwd + const dcgResult = await runDcg(binaryPath, canonicalCommand, workingDirectory) + dcgBlocked = dcgResult.decision === "deny" + if (dcgResult.decision === "deny") { + await task.say("error", formatDcgBlockedMessage(dcgResult.reason, dcgResult.ruleId)) + } + } + + // DCG-approved commands are auto-approved by checkAutoApproval. A DCG + // block is presented as Zoo's normal command prompt, with isProtected + // forcing the user to explicitly choose whether to execute it. + const didApprove = dcgBlocked + ? await askApproval("command", canonicalCommand, undefined, true) + : await askApproval("command", canonicalCommand) if (!didApprove) { return } const executionId = task.lastMessageTs?.toString() ?? Date.now().toString() - const provider = await task.providerRef.deref() - const providerState = await provider?.getState() - const { terminalShellIntegrationDisabled = true } = providerState ?? {} // Get command execution timeout from VSCode configuration (in seconds) diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index c3236f3565..82522328ae 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -45,6 +45,14 @@ vitest.mock("../../../integrations/terminal/TerminalRegistry", () => ({ vitest.mock("../../task/Task") vitest.mock("../../prompts/responses") +const mockRunDcg = vitest.fn() +const mockEnsureDcgInstalled = vitest.fn() + +vitest.mock("../../../services/destructive-command-guard", () => ({ + runDcg: mockRunDcg, + ensureDcgInstalled: mockEnsureDcgInstalled, +})) + // Import the module import * as executeCommandModule from "../ExecuteCommandTool" const { executeCommandTool } = executeCommandModule @@ -96,6 +104,8 @@ describe("executeCommandTool", () => { mockAskApproval = vitest.fn().mockResolvedValue(true) mockHandleError = vitest.fn().mockResolvedValue(undefined) mockPushToolResult = vitest.fn() + mockRunDcg.mockResolvedValue({ decision: "allow" }) + mockEnsureDcgInstalled.mockResolvedValue("/test/storage/dcg") // Setup vscode config mock const mockConfig = { @@ -199,6 +209,129 @@ describe("executeCommandTool", () => { }) describe("Error handling", () => { + it.each([ + [undefined, undefined, "executeCommand.destructiveCommandGuard.blocked"], + ["matches a destructive pattern", undefined, "executeCommand.destructiveCommandGuard.blockedWithReason"], + [undefined, "recursive-delete", "executeCommand.destructiveCommandGuard.blockedWithRule"], + [ + "matches a destructive pattern", + "recursive-delete", + "executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", + ], + ])("selects the localized DCG block message for reason %s and rule %s", (reason, ruleId, expected) => { + expect(executeCommandModule.formatDcgBlockedMessage(reason, ruleId)).toBe(expected) + }) + + 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" } } + provider.getState.mockResolvedValue({ + destructiveCommandGuardEnabled: true, + terminalShellIntegrationDisabled: true, + }) + mockRunDcg.mockResolvedValue({ + decision: "deny", + reason: "matches a destructive pattern", + ruleId: "recursive-delete", + }) + mockAskApproval.mockResolvedValue(false) + + await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + + expect(mockCline.say).toHaveBeenCalledWith( + "error", + "executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", + ) + expect(mockAskApproval).toHaveBeenCalledWith("command", "echo test", undefined, true) + }) + + it("requests normal approval when DCG allows the command", async () => { + const provider = await mockCline.providerRef.deref() + provider.context = { globalStorageUri: { fsPath: "/test/storage" } } + provider.getState.mockResolvedValue({ + destructiveCommandGuardEnabled: true, + terminalShellIntegrationDisabled: true, + }) + mockRunDcg.mockResolvedValue({ decision: "allow" }) + + await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + + expect(mockAskApproval).toHaveBeenCalledWith("command", "echo test") + }) + + it("installs or updates DCG before evaluating an enabled command", async () => { + const provider = await mockCline.providerRef.deref() + provider.context = { globalStorageUri: { fsPath: "/test/storage" } } + provider.getState.mockResolvedValue({ + destructiveCommandGuardEnabled: true, + terminalShellIntegrationDisabled: true, + }) + await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + + expect(mockEnsureDcgInstalled).toHaveBeenCalledWith("/test/storage") + expect(mockRunDcg).toHaveBeenCalledWith("/test/storage/dcg", "echo test", "/test/workspace") + }) + + it("fails closed when the DCG install or update fails", async () => { + const provider = await mockCline.providerRef.deref() + provider.context = { globalStorageUri: { fsPath: "/test/storage" } } + provider.getState.mockResolvedValue({ + destructiveCommandGuardEnabled: true, + terminalShellIntegrationDisabled: true, + }) + mockEnsureDcgInstalled.mockRejectedValue(new Error("download failed")) + + await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + + expect(mockHandleError).toHaveBeenCalledWith( + "executing command", + expect.objectContaining({ message: "download failed" }), + ) + expect(mockRunDcg).not.toHaveBeenCalled() + expect(mockAskApproval).not.toHaveBeenCalled() + expect(executeCommandModule.executeCommandInTerminal).not.toHaveBeenCalled() + }) + + it("fails closed when DCG is unavailable for the current platform", async () => { + const provider = await mockCline.providerRef.deref() + provider.context = { globalStorageUri: { fsPath: "/test/storage" } } + provider.getState.mockResolvedValue({ + destructiveCommandGuardEnabled: true, + terminalShellIntegrationDisabled: true, + }) + mockEnsureDcgInstalled.mockResolvedValue(undefined) + + await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, { + askApproval: mockAskApproval as unknown as AskApproval, + handleError: mockHandleError as unknown as HandleError, + pushToolResult: mockPushToolResult as unknown as PushToolResult, + }) + + expect(mockHandleError).toHaveBeenCalledWith( + "executing command", + expect.objectContaining({ message: "errors.destructiveCommandGuard.unavailable" }), + ) + expect(mockRunDcg).not.toHaveBeenCalled() + expect(mockAskApproval).not.toHaveBeenCalled() + expect(executeCommandModule.executeCommandInTerminal).not.toHaveBeenCalled() + }) + it("should handle missing command parameter", async () => { // Setup mockToolUse.params.command = undefined diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 93f363cf25..d76adf23bc 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Cercant '{{query}}' a la base de codi..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "No s'ha pogut crear una nova tasca a causa de restriccions de política." diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index c1f358a01e..c3f17707ea 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Suche nach '{{query}}' im Codebase..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Neue Aufgabe konnte aufgrund von Richtlinienbeschränkungen nicht erstellt werden." diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index acde8f23bd..c29c717a87 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Searching for '{{query}}' in codebase..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Failed to create new task due to policy restrictions." diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 48d6e4da39..7739a88834 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Buscando '{{query}}' en la base de código..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "No se pudo crear una nueva tarea debido a restricciones de política." diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index 45d6ba3325..9788adc4d9 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Recherche de '{{query}}' dans la base de code..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Impossible de créer une nouvelle tâche en raison de restrictions de politique." diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index e377c46f09..867d801d03 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "कोडबेस में '{{query}}' खोज रहा है..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया।", + "blockedWithReason": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}}", + "blockedWithRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। (नियम: {{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard ने इस कमांड को ब्लॉक कर दिया। DCG का संदेश: {{reason}} (नियम: {{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "नीति प्रतिबंधों के कारण नया कार्य बनाने में विफल।" diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 04d1d16daa..1a8362ce51 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Mencari '{{query}}' di codebase..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "searchFiles": { "workspaceBoundaryError": "Tidak dapat mencari di luar workspace. Path '{{path}}' berada di luar workspace saat ini." }, diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index e384a0ea80..8df4f267bf 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Ricerca di '{{query}}' nella base di codice..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Impossibile creare una nuova attività a causa di restrizioni di policy." diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index f319b0b768..f8ed987d12 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "コードベースで '{{query}}' を検索中..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard がこのコマンドをブロックしました。", + "blockedWithReason": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}", + "blockedWithRule": "Destructive Command Guard がこのコマンドをブロックしました。(ルール: {{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard がこのコマンドをブロックしました。DCG からのメッセージ: {{reason}}(ルール: {{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "ポリシー制限により新しいタスクを作成できませんでした。" diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 6b8e91726b..7149766678 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "코드베이스에서 '{{query}}' 검색 중..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard가 이 명령어를 차단했습니다.", + "blockedWithReason": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}}", + "blockedWithRule": "Destructive Command Guard가 이 명령어를 차단했습니다. (규칙: {{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard가 이 명령어를 차단했습니다. DCG 메시지: {{reason}} (규칙: {{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "정책 제한으로 인해 새 작업을 생성하지 못했습니다." diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 5a5df015a7..f26f5bd68a 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Zoeken naar '{{query}}' in codebase..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Kan geen nieuwe taak aanmaken vanwege beleidsbeperkingen." diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 32a3e27f49..64743eade9 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Wyszukiwanie '{{query}}' w bazie kodu..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Nie udało się utworzyć nowego zadania z powodu ograniczeń polityki." diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 28e950db9c..97b2581f04 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Pesquisando '{{query}}' na base de código..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Falha ao criar nova tarefa devido a restrições de política." diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 6d20010a45..42688f88bf 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Поиск '{{query}}' в кодовой базе..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard заблокировал эту команду.", + "blockedWithReason": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}}", + "blockedWithRule": "Destructive Command Guard заблокировал эту команду. (Правило: {{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard заблокировал эту команду. Сообщение от DCG: {{reason}} (Правило: {{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "Не удалось создать новую задачу из-за ограничений политики." diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index de447fcf21..0d5813f2ad 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Kod tabanında '{{query}}' aranıyor..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Politika kısıtlamaları nedeniyle yeni görev oluşturulamadı." diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index ec43f2b39e..301fc6a3dc 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "Đang tìm kiếm '{{query}}' trong cơ sở mã..." }, + "executeCommand": { + "destructiveCommandGuard": { + "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}})" + } + }, "newTask": { "errors": { "policy_restriction": "Không thể tạo nhiệm vụ mới do hạn chế chính sách." diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index d5a5701f3d..80342c1891 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "正在搜索代码库中的 '{{query}}'..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard 已拦截此命令。", + "blockedWithReason": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}", + "blockedWithRule": "Destructive Command Guard 已拦截此命令。(规则:{{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard 已拦截此命令。来自 DCG 的消息:{{reason}}(规则:{{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "由于策略限制,无法创建新任务。" diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 02623543b9..e090c2225c 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -13,6 +13,14 @@ "codebaseSearch": { "approval": "正在搜尋程式碼庫中的「{{query}}」..." }, + "executeCommand": { + "destructiveCommandGuard": { + "blocked": "Destructive Command Guard 已阻擋此命令。", + "blockedWithReason": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}", + "blockedWithRule": "Destructive Command Guard 已阻擋此命令。(規則:{{ruleId}})", + "blockedWithReasonAndRule": "Destructive Command Guard 已阻擋此命令。來自 DCG 的訊息:{{reason}}(規則:{{ruleId}})" + } + }, "newTask": { "errors": { "policy_restriction": "由於政策限制,無法建立新工作。" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index f973c7929f..3c48b2fdd1 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -72,6 +72,7 @@ import { Split, ArrowRight, Check, + OctagonX, } from "lucide-react" import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" @@ -290,6 +291,18 @@ export const ChatRowContent = ({ case "mistake_limit_reached": return [null, null] // These will be handled by ErrorRow component case "command": + if (message.autoApprovalDecision === "deny") { + return [ + , + + {t("chat:commandExecution.denied")} + , + ] + } return [ isCommandExecuting ? ( @@ -1603,6 +1616,7 @@ export const ChatRowContent = ({ text={message.text} icon={icon} title={title} + isDenied={message.autoApprovalDecision === "deny"} /> ) case "use_mcp_server": diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index b5c38cd9ef..4884d5a8b6 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -36,11 +36,13 @@ interface CommandExecutionProps { text?: string icon?: JSX.Element | null title?: JSX.Element | null + isDenied?: boolean } -export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => { +export const CommandExecution = ({ executionId, text, icon, title, isDenied = false }: CommandExecutionProps) => { const { terminalShellIntegrationDisabled = false, + destructiveCommandGuardEnabled = false, allowedCommands = [], deniedCommands = [], setAllowedCommands, @@ -107,8 +109,8 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec } const handleDenyPatternChange = (pattern: string) => { - const isDenied = deniedCommands.includes(pattern) - const newDenied = isDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern] + const isPatternDenied = deniedCommands.includes(pattern) + const newDenied = isPatternDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern] const newAllowed = allowedCommands.filter((p) => p !== pattern) setAllowedCommands(newAllowed) @@ -245,7 +247,7 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec - {command && command.trim() && ( + {command && command.trim() && !destructiveCommandGuardEnabled && !isDenied && ( ({ + useTranslation: () => ({ t: (key: string) => key }), + Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => <>{children || i18nKey}, + initReactI18next: { type: "3rdParty", init: () => {} }, +})) + +vi.mock("../CommandExecution", () => ({ + CommandExecution: ({ text, title, isDenied }: { text?: string; title?: React.ReactNode; isDenied?: boolean }) => ( +
+ {title} + {text} +
+ ), +})) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeBadge: ({ children, ...props }: { children: React.ReactNode }) => {children}, +})) + +const renderCommand = (autoApprovalDecision?: "approve" | "deny") => { + const queryClient = new QueryClient() + + return render( + + + + + , + ) +} + +describe("ChatRow - denied commands", () => { + it("shows a denied status for a command rejected by auto-approval", () => { + renderCommand("deny") + + expect(screen.getByText("chat:commandExecution.denied")).toBeInTheDocument() + expect(screen.getByTestId("command-execution")).toHaveAttribute("data-denied", "true") + }) + + it("does not show a denied status for an approved command", () => { + renderCommand("approve") + + expect(screen.queryByText("chat:commandExecution.denied")).not.toBeInTheDocument() + expect(screen.getByTestId("command-execution")).toHaveAttribute("data-denied", "false") + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx index 19d2ca2c5c..910d903a43 100644 --- a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx @@ -44,6 +44,7 @@ vi.mock("../CommandPatternSelector", () => ({ // Mock ExtensionStateContext const mockExtensionState = { terminalShellIntegrationDisabled: false, + destructiveCommandGuardEnabled: false, allowedCommands: ["npm"], deniedCommands: ["rm"], setAllowedCommands: vi.fn(), @@ -110,6 +111,33 @@ describe("CommandExecution", () => { expect(selector).toHaveTextContent("npm install express") }) + it("should hide the command pattern selector while destructive command guard is enabled", () => { + const state = { + ...mockExtensionState, + destructiveCommandGuardEnabled: true, + } + + render( + + + , + ) + + expect(screen.getByTestId("code-block")).toHaveTextContent("npm install express") + expect(screen.queryByTestId("command-pattern-selector")).not.toBeInTheDocument() + }) + + it("should hide the command pattern selector for a denied command", () => { + render( + + + , + ) + + expect(screen.getByTestId("code-block")).toHaveTextContent("rm -rf /tmp/example") + expect(screen.queryByTestId("command-pattern-selector")).not.toBeInTheDocument() + }) + it("should handle allow command change", () => { render( diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 11ac1d4028..6460895fd3 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -233,6 +233,7 @@ "exitStatus": "S'ha sortit amb l'estat {{exitCode}}", "malformedCommand": "Ordre mal formada: error de sintaxi del shell", "manageCommands": "Comandes aprovades automàticament", + "denied": "Ordre denegada", "addToAllowed": "Afegeix a la llista permesa", "removeFromAllowed": "Elimina de la llista permesa", "addToDenied": "Afegeix a la llista denegada", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 38415c87a5..a2201231bc 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Beendet mit Status {{exitCode}}", "malformedCommand": "Fehlerhafter Befehl: Shell-Syntaxfehler", "manageCommands": "Automatisch genehmigte Befehle", + "denied": "Befehl abgelehnt", "addToAllowed": "Zur erlaubten Liste hinzufügen", "removeFromAllowed": "Von der erlaubten Liste entfernen", "addToDenied": "Zur verweigerten Liste hinzufügen", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 8df0c2938a..5775f2bee6 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -276,6 +276,7 @@ "exitStatus": "Exited with status {{exitCode}}", "malformedCommand": "Malformed command: shell syntax error", "manageCommands": "Auto-approved commands", + "denied": "Command denied", "addToAllowed": "Add to allowed list", "removeFromAllowed": "Remove from allowed list", "addToDenied": "Add to denied list", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index d3beb38fe0..b5e5f6bb24 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Salió con el estado {{exitCode}}", "malformedCommand": "Comando mal formado: error de sintaxis del shell", "manageCommands": "Comandos aprobados automáticamente", + "denied": "Comando denegado", "addToAllowed": "Agregar a la lista de permitidos", "removeFromAllowed": "Eliminar de la lista de permitidos", "addToDenied": "Agregar a la lista de denegados", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 080e3cb044..73a1f523ee 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Terminé avec le statut {{exitCode}}", "malformedCommand": "Commande mal formée : erreur de syntaxe shell", "manageCommands": "Commandes approuvées automatiquement", + "denied": "Commande refusée", "addToAllowed": "Ajouter à la liste autorisée", "removeFromAllowed": "Retirer de la liste autorisée", "addToDenied": "Ajouter à la liste refusée", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index f2ac1cb20d..c5a877a7b8 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -233,6 +233,7 @@ "exitStatus": "{{exitCode}} स्थिति के साथ बाहर निकल गया", "malformedCommand": "गलत कमांड: shell सिंटैक्स त्रुटि", "manageCommands": "स्वतः-अनुमोदित कमांड", + "denied": "कमांड अस्वीकृत", "addToAllowed": "अनुमत सूची में जोड़ें", "removeFromAllowed": "अनुमत सूची से हटाएँ", "addToDenied": "अस्वीकृत सूची में जोड़ें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index e95a517221..f9b73ac16e 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -284,6 +284,7 @@ "exitStatus": "Keluar dengan status {{exitCode}}", "malformedCommand": "Perintah tidak valid: kesalahan sintaks shell", "manageCommands": "Perintah yang disetujui secara otomatis", + "denied": "Perintah ditolak", "addToAllowed": "Tambahkan ke daftar yang diizinkan", "removeFromAllowed": "Hapus dari daftar yang diizinkan", "addToDenied": "Tambahkan ke daftar yang ditolak", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 7085edee5b..772e4f4c6b 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Uscito con stato {{exitCode}}", "malformedCommand": "Comando non valido: errore di sintassi della shell", "manageCommands": "Comandi approvati automaticamente", + "denied": "Comando negato", "addToAllowed": "Aggiungi alla lista dei permessi", "removeFromAllowed": "Rimuovi dalla lista dei permessi", "addToDenied": "Aggiungi alla lista dei negati", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 8abbe5792c..5c8897d63e 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -233,6 +233,7 @@ "exitStatus": "ステータス {{exitCode}} で終了しました", "malformedCommand": "不正なコマンド: シェル構文エラー", "manageCommands": "自動承認されたコマンド", + "denied": "コマンドが拒否されました", "addToAllowed": "許可リストに追加", "removeFromAllowed": "許可リストから削除", "addToDenied": "拒否リストに追加", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index ac7b6c4c05..93b9b99e09 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -233,6 +233,7 @@ "exitStatus": "상태 {{exitCode}}(으)로 종료됨", "malformedCommand": "잘못된 명령: shell 구문 오류", "manageCommands": "자동 승인된 명령", + "denied": "명령 거부됨", "addToAllowed": "허용 목록에 추가", "removeFromAllowed": "허용 목록에서 제거", "addToDenied": "거부 목록에 추가", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index aa7608b1f3..f9557cb37f 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -228,6 +228,7 @@ "exitStatus": "Afgesloten met status {{exitCode}}", "malformedCommand": "Ongeldig commando: shell syntaxisfout", "manageCommands": "Automatisch goedgekeurde commando's", + "denied": "Commando geweigerd", "addToAllowed": "Toevoegen aan toegestane lijst", "removeFromAllowed": "Verwijderen van toegestane lijst", "addToDenied": "Toevoegen aan geweigerde lijst", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 505d8b766c..fdc598c814 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Zakończono ze statusem {{exitCode}}", "malformedCommand": "Nieprawidlowe polecenie: blad skladni shell", "manageCommands": "Polecenia zatwierdzone automatycznie", + "denied": "Polecenie odrzucone", "addToAllowed": "Dodaj do listy dozwolonych", "removeFromAllowed": "Usuń z listy dozwolonych", "addToDenied": "Dodaj do listy odrzuconych", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 45e7b227ce..9cb658529d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Saiu com o status {{exitCode}}", "malformedCommand": "Comando malformado: erro de sintaxe do shell", "manageCommands": "Comandos aprovados automaticamente", + "denied": "Comando negado", "addToAllowed": "Adicionar à lista de permitidos", "removeFromAllowed": "Remover da lista de permitidos", "addToDenied": "Adicionar à lista de negados", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 5b8ae0da19..dd118ace19 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -228,6 +228,7 @@ "exitStatus": "Завершено со статусом {{exitCode}}", "malformedCommand": "Некорректная команда: синтаксическая ошибка shell", "manageCommands": "Управление разрешениями команд", + "denied": "Команда запрещена", "commandManagementDescription": "Управляйте разрешениями команд: Нажмите ✓, чтобы разрешить автоматическое выполнение, ✗, чтобы запретить выполнение. Шаблоны можно включать/выключать или удалять из списков. Просмотреть все настройки", "addToAllowed": "Добавить в список разрешенных", "removeFromAllowed": "Удалить из списка разрешенных", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index a5c0bac756..15a199a702 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -233,6 +233,7 @@ "exitStatus": "{{exitCode}} durumuyla çıkıldı", "malformedCommand": "Hatalı komut: shell sozdizimi hatasi", "manageCommands": "Komut İzinlerini Yönet", + "denied": "Komut reddedildi", "commandManagementDescription": "Komut izinlerini yönetin: Otomatik yürütmeye izin vermek için ✓'e, yürütmeyi reddetmek için ✗'e tıklayın. Desenler açılıp kapatılabilir veya listelerden kaldırılabilir. Tüm ayarları görüntüle", "addToAllowed": "İzin verilenler listesine ekle", "removeFromAllowed": "İzin verilenler listesinden kaldır", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index b5da186e43..89e1b08fa9 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -233,6 +233,7 @@ "exitStatus": "Đã thoát với trạng thái {{exitCode}}", "malformedCommand": "Lệnh không hợp lệ: lỗi cú pháp shell", "manageCommands": "Quản lý quyền lệnh", + "denied": "Lệnh bị từ chối", "commandManagementDescription": "Quản lý quyền lệnh: Nhấp vào ✓ để cho phép tự động thực thi, ✗ để từ chối thực thi. Các mẫu có thể được bật/tắt hoặc xóa khỏi danh sách. Xem tất cả cài đặt", "addToAllowed": "Thêm vào danh sách cho phép", "removeFromAllowed": "Xóa khỏi danh sách cho phép", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 63b8ac69fd..3b9e5047e8 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -233,6 +233,7 @@ "exitStatus": "已退出,状态码 {{exitCode}}", "malformedCommand": "命令格式错误:shell 语法错误", "manageCommands": "管理命令权限", + "denied": "命令已拒绝", "commandManagementDescription": "管理命令权限:点击 ✓ 允许自动执行,点击 ✗ 拒绝执行。可以打开/关闭模式或从列表中删除。查看所有设置", "addToAllowed": "添加到允许列表", "removeFromAllowed": "从允许列表中删除", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index dbea150f29..2e85c13b7b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -279,6 +279,7 @@ "exitStatus": "已結束,狀態碼 {{exitCode}}", "malformedCommand": "命令格式錯誤:shell 語法錯誤", "manageCommands": "自動核准的命令", + "denied": "命令已拒絕", "addToAllowed": "新增至允許清單", "removeFromAllowed": "從允許清單中移除", "addToDenied": "新增至拒絕清單",