mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: implement customizable forbidden command messages
- Updated configuration schema to support both string and object formats for denied commands - Added DeniedCommand type to support custom messages per command prefix - Modified frontend components to handle and display custom messages - Updated localization files with new translation keys - Added comprehensive tests for the new functionality - Maintained backward compatibility with existing string-based configuration Fixes #6754
This commit is contained in:
parent
2b647ed9a1
commit
7b3c087231
29 changed files with 537 additions and 94 deletions
|
|
@ -29,6 +29,20 @@ export const DEFAULT_WRITE_DELAY_MS = 1000
|
|||
*/
|
||||
export const DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT = 50_000
|
||||
|
||||
/**
|
||||
* DeniedCommand type - can be either a string (for backward compatibility)
|
||||
* or an object with prefix and optional custom message
|
||||
*/
|
||||
export const deniedCommandSchema = z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
prefix: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
export type DeniedCommand = z.infer<typeof deniedCommandSchema>
|
||||
|
||||
/**
|
||||
* GlobalSettings
|
||||
*/
|
||||
|
|
@ -63,7 +77,7 @@ export const globalSettingsSchema = z.object({
|
|||
followupAutoApproveTimeoutMs: z.number().optional(),
|
||||
alwaysAllowUpdateTodoList: z.boolean().optional(),
|
||||
allowedCommands: z.array(z.string()).optional(),
|
||||
deniedCommands: z.array(z.string()).optional(),
|
||||
deniedCommands: z.array(deniedCommandSchema).optional(),
|
||||
commandExecutionTimeout: z.number().optional(),
|
||||
commandTimeoutAllowlist: z.array(z.string()).optional(),
|
||||
preventCompletionWithOpenTodos: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -1498,8 +1498,62 @@ export class ClineProvider
|
|||
* Merges denied commands from global state and workspace configuration
|
||||
* with proper validation and deduplication
|
||||
*/
|
||||
private mergeDeniedCommands(globalStateCommands?: string[]): string[] {
|
||||
return this.mergeCommandLists("deniedCommands", "denied", globalStateCommands)
|
||||
private mergeDeniedCommands(
|
||||
globalStateCommands?: (string | { prefix: string; message?: string })[],
|
||||
): (string | { prefix: string; message?: string })[] {
|
||||
try {
|
||||
// Validate and sanitize global state commands
|
||||
const validGlobalCommands = Array.isArray(globalStateCommands)
|
||||
? globalStateCommands.filter((cmd) => {
|
||||
if (typeof cmd === "string") {
|
||||
return cmd.trim().length > 0
|
||||
} else if (typeof cmd === "object" && cmd !== null && "prefix" in cmd) {
|
||||
return typeof cmd.prefix === "string" && cmd.prefix.trim().length > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
: []
|
||||
|
||||
// Get workspace configuration commands
|
||||
const workspaceCommands =
|
||||
vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.get<(string | { prefix: string; message?: string })[]>("deniedCommands") || []
|
||||
|
||||
// Validate and sanitize workspace commands
|
||||
const validWorkspaceCommands = Array.isArray(workspaceCommands)
|
||||
? workspaceCommands.filter((cmd) => {
|
||||
if (typeof cmd === "string") {
|
||||
return cmd.trim().length > 0
|
||||
} else if (typeof cmd === "object" && cmd !== null && "prefix" in cmd) {
|
||||
return typeof cmd.prefix === "string" && cmd.prefix.trim().length > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
: []
|
||||
|
||||
// Combine and deduplicate commands
|
||||
// Global state takes precedence over workspace configuration
|
||||
const prefixMap = new Map<string, string | { prefix: string; message?: string }>()
|
||||
|
||||
// Add workspace commands first
|
||||
validWorkspaceCommands.forEach((cmd) => {
|
||||
const prefix = typeof cmd === "string" ? cmd : cmd.prefix
|
||||
prefixMap.set(prefix, cmd)
|
||||
})
|
||||
|
||||
// Add global commands (overwriting workspace if same prefix)
|
||||
validGlobalCommands.forEach((cmd) => {
|
||||
const prefix = typeof cmd === "string" ? cmd : cmd.prefix
|
||||
prefixMap.set(prefix, cmd)
|
||||
})
|
||||
|
||||
return Array.from(prefixMap.values())
|
||||
} catch (error) {
|
||||
console.error(`Error merging denied commands:`, error)
|
||||
// Return empty array as fallback to prevent crashes
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
type GlobalState,
|
||||
type ClineMessage,
|
||||
TelemetryEventName,
|
||||
type DeniedCommand,
|
||||
} from "@roo-code/types"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
|
@ -751,7 +752,7 @@ export const webviewMessageHandler = async (
|
|||
? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
|
||||
: []
|
||||
|
||||
await updateGlobalState("allowedCommands", validCommands)
|
||||
await updateGlobalState("allowedCommands", validCommands as string[])
|
||||
|
||||
// Also update workspace settings.
|
||||
await vscode.workspace
|
||||
|
|
@ -761,10 +762,18 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
}
|
||||
case "deniedCommands": {
|
||||
// Validate and sanitize the commands array
|
||||
// Validate and sanitize the commands array - now supports both string and object format
|
||||
const commands = message.commands ?? []
|
||||
const validCommands = Array.isArray(commands)
|
||||
? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
|
||||
? commands.filter((cmd): cmd is string | DeniedCommand => {
|
||||
if (typeof cmd === "string") {
|
||||
return cmd.trim().length > 0
|
||||
} else if (typeof cmd === "object" && cmd !== null && "prefix" in cmd) {
|
||||
// Validate object format
|
||||
return typeof cmd.prefix === "string" && cmd.prefix.trim().length > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
: []
|
||||
|
||||
await updateGlobalState("deniedCommands", validCommands)
|
||||
|
|
@ -772,7 +781,7 @@ export const webviewMessageHandler = async (
|
|||
// Also update workspace settings.
|
||||
await vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global)
|
||||
.update("deniedCommands", validCommands as any, vscode.ConfigurationTarget.Global)
|
||||
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,7 +333,28 @@
|
|||
"roo-cline.deniedCommands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"description": "The command prefix to deny"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Custom message to show when this command is denied"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prefix"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": [],
|
||||
"description": "%commands.deniedCommands.description%"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
InstallMarketplaceItemOptions,
|
||||
MarketplaceItem,
|
||||
ShareVisibility,
|
||||
DeniedCommand,
|
||||
} from "@roo-code/types"
|
||||
import { marketplaceItemSchema } from "@roo-code/types"
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ export interface WebviewMessage {
|
|||
images?: string[]
|
||||
bool?: boolean
|
||||
value?: number
|
||||
commands?: string[]
|
||||
commands?: string[] | DeniedCommand[]
|
||||
audioType?: AudioType
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { vscode } from "@src/utils/vscode"
|
|||
import {
|
||||
getCommandDecision,
|
||||
CommandDecision,
|
||||
findLongestPrefixMatch,
|
||||
findLongestDeniedMatch,
|
||||
parseCommand,
|
||||
} from "@src/utils/command-validation"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
|
@ -1077,17 +1077,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
[getCommandDecisionForMessage],
|
||||
)
|
||||
|
||||
// Helper function to get the denied prefix for a command
|
||||
const getDeniedPrefix = useCallback(
|
||||
(command: string): string | null => {
|
||||
// Helper function to get the denied command info (prefix and optional message) for a command
|
||||
const getDeniedCommandInfo = useCallback(
|
||||
(command: string): { prefix: string; message?: string } | null => {
|
||||
if (!command || !deniedCommands?.length) return null
|
||||
|
||||
// Parse the command into sub-commands and check each one
|
||||
const subCommands = parseCommand(command)
|
||||
for (const cmd of subCommands) {
|
||||
const deniedMatch = findLongestPrefixMatch(cmd, deniedCommands)
|
||||
if (deniedMatch) {
|
||||
return deniedMatch
|
||||
const deniedMatch = findLongestDeniedMatch(cmd, deniedCommands)
|
||||
if (deniedMatch.prefix) {
|
||||
return deniedMatch as { prefix: string; message?: string }
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
|
@ -1606,11 +1606,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const autoApproveOrReject = async () => {
|
||||
// Check for auto-reject first (commands that should be denied)
|
||||
if (lastMessage?.ask === "command" && isDeniedCommand(lastMessage)) {
|
||||
// Get the denied prefix for the localized message
|
||||
const deniedPrefix = getDeniedPrefix(lastMessage.text || "")
|
||||
if (deniedPrefix) {
|
||||
// Create the localized auto-deny message and send it with the rejection
|
||||
const autoDenyMessage = tSettings("autoApprove.execute.autoDenied", { prefix: deniedPrefix })
|
||||
// Get the denied command info (prefix and optional custom message)
|
||||
const deniedInfo = getDeniedCommandInfo(lastMessage.text || "")
|
||||
if (deniedInfo) {
|
||||
// Use custom message if provided, otherwise use the default localized message
|
||||
const autoDenyMessage =
|
||||
deniedInfo.message || tSettings("autoApprove.execute.autoDenied", { prefix: deniedInfo.prefix })
|
||||
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
|
|
@ -1710,7 +1711,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleSuggestionClickInRow,
|
||||
isAllowedCommand,
|
||||
isDeniedCommand,
|
||||
getDeniedPrefix,
|
||||
getDeniedCommandInfo,
|
||||
tSettings,
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { cn } from "@src/lib/utils"
|
|||
import { Button } from "@src/components/ui"
|
||||
import CodeBlock from "../common/CodeBlock"
|
||||
import { CommandPatternSelector } from "./CommandPatternSelector"
|
||||
import { parseCommand } from "../../utils/command-validation"
|
||||
import { parseCommand, deniedCommandsToPrefixes } from "../../utils/command-validation"
|
||||
import { extractPatternsFromCommand } from "../../utils/command-parser"
|
||||
|
||||
interface CommandPattern {
|
||||
|
|
@ -82,7 +82,10 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
const handleAllowPatternChange = (pattern: string) => {
|
||||
const isAllowed = allowedCommands.includes(pattern)
|
||||
const newAllowed = isAllowed ? allowedCommands.filter((p) => p !== pattern) : [...allowedCommands, pattern]
|
||||
const newDenied = deniedCommands.filter((p) => p !== pattern)
|
||||
const newDenied = deniedCommands.filter((cmd) => {
|
||||
const prefix = typeof cmd === "string" ? cmd : cmd.prefix
|
||||
return prefix !== pattern
|
||||
})
|
||||
|
||||
setAllowedCommands(newAllowed)
|
||||
setDeniedCommands(newDenied)
|
||||
|
|
@ -91,8 +94,14 @@ 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 deniedPrefixes = deniedCommandsToPrefixes(deniedCommands)
|
||||
const isDenied = deniedPrefixes.includes(pattern)
|
||||
const newDenied = isDenied
|
||||
? deniedCommands.filter((cmd) => {
|
||||
const prefix = typeof cmd === "string" ? cmd : cmd.prefix
|
||||
return prefix !== pattern
|
||||
})
|
||||
: [...deniedCommands, pattern]
|
||||
const newAllowed = allowedCommands.filter((p) => p !== pattern)
|
||||
|
||||
setAllowedCommands(newAllowed)
|
||||
|
|
@ -194,7 +203,7 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
<CommandPatternSelector
|
||||
patterns={commandPatterns}
|
||||
allowedCommands={allowedCommands}
|
||||
deniedCommands={deniedCommands}
|
||||
deniedCommands={deniedCommandsToPrefixes(deniedCommands)}
|
||||
onAllowPatternChange={handleAllowPatternChange}
|
||||
onDenyPatternChange={handleDenyPatternChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
|
|||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { Button, Input, Slider, StandardTooltip } from "@/components/ui"
|
||||
import type { DeniedCommand } from "@roo-code/types"
|
||||
|
||||
import { SetCachedStateField } from "./types"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
|
|
@ -34,7 +35,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
allowedCommands?: string[]
|
||||
allowedMaxRequests?: number | undefined
|
||||
allowedMaxCost?: number | undefined
|
||||
deniedCommands?: string[]
|
||||
deniedCommands?: DeniedCommand[]
|
||||
setCachedStateField: SetCachedStateField<
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowReadOnlyOutsideWorkspace"
|
||||
|
|
@ -84,6 +85,8 @@ export const AutoApproveSettings = ({
|
|||
const { t } = useAppTranslation()
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
const [deniedCommandInput, setDeniedCommandInput] = useState("")
|
||||
const [deniedCommandMessage, setDeniedCommandMessage] = useState("")
|
||||
const [showCustomMessage, setShowCustomMessage] = useState(false)
|
||||
const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState()
|
||||
|
||||
const toggles = useAutoApprovalToggles()
|
||||
|
|
@ -104,11 +107,26 @@ export const AutoApproveSettings = ({
|
|||
const handleAddDeniedCommand = () => {
|
||||
const currentCommands = deniedCommands ?? []
|
||||
|
||||
if (deniedCommandInput && !currentCommands.includes(deniedCommandInput)) {
|
||||
const newCommands = [...currentCommands, deniedCommandInput]
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
setDeniedCommandInput("")
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
if (deniedCommandInput) {
|
||||
// Check if this prefix already exists
|
||||
const existingIndex = currentCommands.findIndex(
|
||||
(cmd) => (typeof cmd === "string" ? cmd : cmd.prefix) === deniedCommandInput,
|
||||
)
|
||||
|
||||
if (existingIndex === -1) {
|
||||
// Add new denied command
|
||||
const newCommand: DeniedCommand =
|
||||
showCustomMessage && deniedCommandMessage.trim()
|
||||
? { prefix: deniedCommandInput, message: deniedCommandMessage.trim() }
|
||||
: deniedCommandInput
|
||||
|
||||
const newCommands = [...currentCommands, newCommand]
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
setDeniedCommandInput("")
|
||||
setDeniedCommandMessage("")
|
||||
setShowCustomMessage(false)
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,45 +374,94 @@ export const AutoApproveSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={deniedCommandInput}
|
||||
onChange={(e: any) => setDeniedCommandInput(e.target.value)}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
handleAddDeniedCommand()
|
||||
}
|
||||
}}
|
||||
placeholder={t("settings:autoApprove.execute.deniedCommandPlaceholder")}
|
||||
className="grow"
|
||||
data-testid="denied-command-input"
|
||||
/>
|
||||
<Button
|
||||
className="h-8"
|
||||
onClick={handleAddDeniedCommand}
|
||||
data-testid="add-denied-command-button">
|
||||
{t("settings:autoApprove.execute.addButton")}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={deniedCommandInput}
|
||||
onChange={(e: any) => setDeniedCommandInput(e.target.value)}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter" && !showCustomMessage) {
|
||||
e.preventDefault()
|
||||
handleAddDeniedCommand()
|
||||
}
|
||||
}}
|
||||
placeholder={t("settings:autoApprove.execute.deniedCommandPlaceholder")}
|
||||
className="grow"
|
||||
data-testid="denied-command-input"
|
||||
/>
|
||||
<Button
|
||||
className="h-8"
|
||||
onClick={handleAddDeniedCommand}
|
||||
data-testid="add-denied-command-button">
|
||||
{t("settings:autoApprove.execute.addButton")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={showCustomMessage}
|
||||
onChange={(e: any) => {
|
||||
setShowCustomMessage(e.target.checked)
|
||||
if (!e.target.checked) {
|
||||
setDeniedCommandMessage("")
|
||||
}
|
||||
}}
|
||||
data-testid="custom-message-checkbox">
|
||||
<span className="text-sm">
|
||||
{t("settings:autoApprove.execute.useCustomMessage")}
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
|
||||
{showCustomMessage && (
|
||||
<Input
|
||||
value={deniedCommandMessage}
|
||||
onChange={(e: any) => setDeniedCommandMessage(e.target.value)}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
handleAddDeniedCommand()
|
||||
}
|
||||
}}
|
||||
placeholder={t("settings:autoApprove.execute.customMessagePlaceholder")}
|
||||
className="w-full"
|
||||
data-testid="custom-message-input"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(deniedCommands ?? []).map((cmd, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
data-testid={`remove-denied-command-${index}`}
|
||||
onClick={() => {
|
||||
const newCommands = (deniedCommands ?? []).filter((_, i) => i !== index)
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
}}>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div>{cmd}</div>
|
||||
<X className="text-foreground scale-75" />
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
{(deniedCommands ?? []).map((cmd, index) => {
|
||||
const prefix = typeof cmd === "string" ? cmd : cmd.prefix
|
||||
const hasCustomMessage = typeof cmd === "object" && cmd.message
|
||||
|
||||
return (
|
||||
<StandardTooltip
|
||||
key={index}
|
||||
content={
|
||||
hasCustomMessage
|
||||
? cmd.message
|
||||
: t("settings:autoApprove.execute.defaultDeniedMessage", { prefix })
|
||||
}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-testid={`remove-denied-command-${index}`}
|
||||
onClick={() => {
|
||||
const newCommands = (deniedCommands ?? []).filter((_, i) => i !== index)
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
}}>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div>{prefix}</div>
|
||||
{hasCustomMessage && (
|
||||
<span className="text-vscode-descriptionForeground">*</span>
|
||||
)}
|
||||
<X className="text-foreground scale-75" />
|
||||
</div>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
type ModeConfig,
|
||||
type ExperimentId,
|
||||
type OrganizationAllowList,
|
||||
type DeniedCommand,
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setShowRooIgnoredFiles: (value: boolean) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setAllowedCommands: (value: string[]) => void
|
||||
setDeniedCommands: (value: string[]) => void
|
||||
setDeniedCommands: (value: DeniedCommand[]) => void
|
||||
setAllowedMaxRequests: (value: number | undefined) => void
|
||||
setAllowedMaxCost: (value: number | undefined) => void
|
||||
setSoundEnabled: (value: boolean) => void
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ca/settings.json
generated
6
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Introduïu prefix de comanda (ex. 'git ')",
|
||||
"deniedCommandPlaceholder": "Introduïu prefix de comanda a denegar (ex. 'rm -rf')",
|
||||
"addButton": "Afegir",
|
||||
"autoDenied": "Les comandes amb el prefix `{{prefix}}` han estat prohibides per l'usuari. No eludeixis aquesta restricció executant una altra comanda."
|
||||
"autoDenied": "Les comandes amb el prefix `{{prefix}}` han estat prohibides per l'usuari. No eludeixis aquesta restricció executant una altra comanda.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/de/settings.json
generated
6
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Befehlspräfix eingeben (z.B. 'git ')",
|
||||
"deniedCommandPlaceholder": "Befehlspräfix zum Verweigern eingeben (z.B. 'rm -rf')",
|
||||
"addButton": "Hinzufügen",
|
||||
"autoDenied": "Befehle mit dem Präfix `{{prefix}}` wurden vom Benutzer verboten. Umgehe diese Beschränkung nicht durch das Ausführen eines anderen Befehls."
|
||||
"autoDenied": "Befehle mit dem Präfix `{{prefix}}` wurden vom Benutzer verboten. Umgehe diese Beschränkung nicht durch das Ausführen eines anderen Befehls.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
|
|
@ -185,7 +185,11 @@
|
|||
"commandPlaceholder": "Enter command prefix (e.g., 'git ')",
|
||||
"deniedCommandPlaceholder": "Enter command prefix to deny (e.g., 'rm -rf')",
|
||||
"addButton": "Add",
|
||||
"autoDenied": "Commands with the prefix `{{prefix}}` have been forbidden by the user. Do not bypass this restriction by running another command."
|
||||
"autoDenied": "Commands with the prefix `{{prefix}}` have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/es/settings.json
generated
6
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Ingrese prefijo de comando (ej. 'git ')",
|
||||
"deniedCommandPlaceholder": "Ingrese prefijo de comando a denegar (ej. 'rm -rf')",
|
||||
"addButton": "Añadir",
|
||||
"autoDenied": "Los comandos con el prefijo `{{prefix}}` han sido prohibidos por el usuario. No eludes esta restricción ejecutando otro comando."
|
||||
"autoDenied": "Los comandos con el prefijo `{{prefix}}` han sido prohibidos por el usuario. No eludes esta restricción ejecutando otro comando.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/fr/settings.json
generated
6
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -188,7 +188,11 @@
|
|||
"commandPlaceholder": "Entrez le préfixe de commande (ex. 'git ')",
|
||||
"deniedCommandPlaceholder": "Entrez le préfixe de commande à refuser (ex. 'rm -rf')",
|
||||
"addButton": "Ajouter",
|
||||
"autoDenied": "Les commandes avec le préfixe `{{prefix}}` ont été interdites par l'utilisateur. Ne contourne pas cette restriction en exécutant une autre commande."
|
||||
"autoDenied": "Les commandes avec le préfixe `{{prefix}}` ont été interdites par l'utilisateur. Ne contourne pas cette restriction en exécutant une autre commande.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/hi/settings.json
generated
6
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "कमांड प्रीफिक्स दर्ज करें (उदा. 'git ')",
|
||||
"deniedCommandPlaceholder": "अस्वीकार करने के लिए कमांड प्रीफिक्स दर्ज करें (उदा. 'rm -rf')",
|
||||
"addButton": "जोड़ें",
|
||||
"autoDenied": "प्रीफिक्स `{{prefix}}` वाले कमांड उपयोगकर्ता द्वारा प्रतिबंधित किए गए हैं। दूसरा कमांड चलाकर इस प्रतिबंध को दरकिनार न करें।"
|
||||
"autoDenied": "प्रीफिक्स `{{prefix}}` वाले कमांड उपयोगकर्ता द्वारा प्रतिबंधित किए गए हैं। दूसरा कमांड चलाकर इस प्रतिबंध को दरकिनार न करें।",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "टूडू",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/id/settings.json
generated
6
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Masukkan prefix perintah (misalnya, 'git ')",
|
||||
"deniedCommandPlaceholder": "Masukkan prefix perintah untuk ditolak (misalnya, 'rm -rf')",
|
||||
"addButton": "Tambah",
|
||||
"autoDenied": "Perintah dengan awalan `{{prefix}}` telah dilarang oleh pengguna. Jangan menghindari pembatasan ini dengan menjalankan perintah lain."
|
||||
"autoDenied": "Perintah dengan awalan `{{prefix}}` telah dilarang oleh pengguna. Jangan menghindari pembatasan ini dengan menjalankan perintah lain.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"showMenu": {
|
||||
"label": "Tampilkan menu auto-approve di tampilan chat",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/it/settings.json
generated
6
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Inserisci prefisso comando (es. 'git ')",
|
||||
"deniedCommandPlaceholder": "Inserisci prefisso comando da negare (es. 'rm -rf')",
|
||||
"addButton": "Aggiungi",
|
||||
"autoDenied": "I comandi con il prefisso `{{prefix}}` sono stati vietati dall'utente. Non aggirare questa restrizione eseguendo un altro comando."
|
||||
"autoDenied": "I comandi con il prefisso `{{prefix}}` sono stati vietati dall'utente. Non aggirare questa restrizione eseguendo un altro comando.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ja/settings.json
generated
6
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "コマンドプレフィックスを入力(例:'git ')",
|
||||
"deniedCommandPlaceholder": "拒否するコマンドプレフィックスを入力(例:'rm -rf')",
|
||||
"addButton": "追加",
|
||||
"autoDenied": "プレフィックス `{{prefix}}` を持つコマンドはユーザーによって禁止されています。別のコマンドを実行してこの制限を回避しないでください。"
|
||||
"autoDenied": "プレフィックス `{{prefix}}` を持つコマンドはユーザーによって禁止されています。別のコマンドを実行してこの制限を回避しないでください。",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ko/settings.json
generated
6
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "명령 접두사 입력(예: 'git ')",
|
||||
"deniedCommandPlaceholder": "거부할 명령 접두사 입력(예: 'rm -rf')",
|
||||
"addButton": "추가",
|
||||
"autoDenied": "접두사 `{{prefix}}`를 가진 명령어는 사용자에 의해 금지되었습니다. 다른 명령어를 실행하여 이 제한을 우회하지 마세요."
|
||||
"autoDenied": "접두사 `{{prefix}}`를 가진 명령어는 사용자에 의해 금지되었습니다. 다른 명령어를 실행하여 이 제한을 우회하지 마세요.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/nl/settings.json
generated
6
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Voer commando-prefix in (bijv. 'git ')",
|
||||
"deniedCommandPlaceholder": "Voer te weigeren commando-prefix in (bijv. 'rm -rf')",
|
||||
"addButton": "Toevoegen",
|
||||
"autoDenied": "Commando's met het prefix `{{prefix}}` zijn verboden door de gebruiker. Omzeil deze beperking niet door een ander commando uit te voeren."
|
||||
"autoDenied": "Commando's met het prefix `{{prefix}}` zijn verboden door de gebruiker. Omzeil deze beperking niet door een ander commando uit te voeren.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/pl/settings.json
generated
6
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Wprowadź prefiks polecenia (np. 'git ')",
|
||||
"deniedCommandPlaceholder": "Wprowadź prefiks polecenia do odrzucenia (np. 'rm -rf')",
|
||||
"addButton": "Dodaj",
|
||||
"autoDenied": "Polecenia z prefiksem `{{prefix}}` zostały zabronione przez użytkownika. Nie obchodź tego ograniczenia uruchamiając inne polecenie."
|
||||
"autoDenied": "Polecenia z prefiksem `{{prefix}}` zostały zabronione przez użytkownika. Nie obchodź tego ograniczenia uruchamiając inne polecenie.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
6
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Digite o prefixo do comando (ex. 'git ')",
|
||||
"deniedCommandPlaceholder": "Digite o prefixo do comando para negar (ex. 'rm -rf')",
|
||||
"addButton": "Adicionar",
|
||||
"autoDenied": "Comandos com o prefixo `{{prefix}}` foram proibidos pelo usuário. Não contorne esta restrição executando outro comando."
|
||||
"autoDenied": "Comandos com o prefixo `{{prefix}}` foram proibidos pelo usuário. Não contorne esta restrição executando outro comando.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ru/settings.json
generated
6
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Введите префикс команды (например, 'git ')",
|
||||
"deniedCommandPlaceholder": "Введите префикс команды для запрета (например, 'rm -rf')",
|
||||
"addButton": "Добавить",
|
||||
"autoDenied": "Команды с префиксом `{{prefix}}` были запрещены пользователем. Не обходи это ограничение, выполняя другую команду."
|
||||
"autoDenied": "Команды с префиксом `{{prefix}}` были запрещены пользователем. Не обходи это ограничение, выполняя другую команду.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/tr/settings.json
generated
6
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Komut öneki girin (örn. 'git ')",
|
||||
"deniedCommandPlaceholder": "Reddetmek için komut öneki girin (örn. 'rm -rf')",
|
||||
"addButton": "Ekle",
|
||||
"autoDenied": "`{{prefix}}` önekli komutlar kullanıcı tarafından yasaklandı. Başka bir komut çalıştırarak bu kısıtlamayı aşma."
|
||||
"autoDenied": "`{{prefix}}` önekli komutlar kullanıcı tarafından yasaklandı. Başka bir komut çalıştırarak bu kısıtlamayı aşma.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/vi/settings.json
generated
6
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "Nhập tiền tố lệnh (ví dụ: 'git ')",
|
||||
"deniedCommandPlaceholder": "Nhập tiền tố lệnh để từ chối (ví dụ: 'rm -rf')",
|
||||
"addButton": "Thêm",
|
||||
"autoDenied": "Các lệnh có tiền tố `{{prefix}}` đã bị người dùng cấm. Đừng vượt qua hạn chế này bằng cách chạy lệnh khác."
|
||||
"autoDenied": "Các lệnh có tiền tố `{{prefix}}` đã bị người dùng cấm. Đừng vượt qua hạn chế này bằng cách chạy lệnh khác.",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "Todo",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
6
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "输入命令前缀(例如 'git ')",
|
||||
"deniedCommandPlaceholder": "输入要拒绝的命令前缀(例如 'rm -rf')",
|
||||
"addButton": "添加",
|
||||
"autoDenied": "前缀为 `{{prefix}}` 的命令已被用户禁止。不要通过运行其他命令来绕过此限制。"
|
||||
"autoDenied": "前缀为 `{{prefix}}` 的命令已被用户禁止。不要通过运行其他命令来绕过此限制。",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "待办",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
6
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -187,7 +187,11 @@
|
|||
"commandPlaceholder": "輸入命令前綴(例如 'git ')",
|
||||
"deniedCommandPlaceholder": "輸入要拒絕的命令前綴(例如 'rm -rf')",
|
||||
"addButton": "新增",
|
||||
"autoDenied": "前綴為 `{{prefix}}` 的命令已被使用者禁止。不要透過執行其他命令來繞過此限制。"
|
||||
"autoDenied": "前綴為 `{{prefix}}` 的命令已被使用者禁止。不要透過執行其他命令來繞過此限制。",
|
||||
"useCustomMessage": "Use custom message",
|
||||
"customMessagePlaceholder": "Enter custom message for this command prefix",
|
||||
"defaultDeniedMessage": "Commands with the prefix '{{prefix}}' have been forbidden by the user. Do not bypass this restriction by running another command.",
|
||||
"hasCustomMessage": "Has custom message"
|
||||
},
|
||||
"updateTodoList": {
|
||||
"label": "待辦",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
CommandValidator,
|
||||
createCommandValidator,
|
||||
containsSubshell,
|
||||
findLongestDeniedMatch,
|
||||
deniedCommandsToPrefixes,
|
||||
} from "../command-validation"
|
||||
|
||||
describe("Command Validation", () => {
|
||||
|
|
@ -1189,3 +1191,142 @@ describe("Unified Command Decision Functions", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Custom Denied Command Messages", () => {
|
||||
describe("deniedCommandsToPrefixes", () => {
|
||||
it("converts string array to prefixes", () => {
|
||||
const result = deniedCommandsToPrefixes(["rm", "sudo", "chmod"])
|
||||
expect(result).toEqual(["rm", "sudo", "chmod"])
|
||||
})
|
||||
|
||||
it("converts DeniedCommand objects to prefixes", () => {
|
||||
const result = deniedCommandsToPrefixes([
|
||||
{ prefix: "rm", message: "Dangerous!" },
|
||||
{ prefix: "sudo" },
|
||||
"chmod",
|
||||
])
|
||||
expect(result).toEqual(["rm", "sudo", "chmod"])
|
||||
})
|
||||
|
||||
it("handles empty array", () => {
|
||||
const result = deniedCommandsToPrefixes([])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("handles undefined", () => {
|
||||
const result = deniedCommandsToPrefixes(undefined as any)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("findLongestDeniedMatch", () => {
|
||||
it("returns null when no match found", () => {
|
||||
const deniedCommands = ["rm", "sudo"]
|
||||
const result = findLongestDeniedMatch("echo hello", deniedCommands)
|
||||
expect(result).toEqual({ prefix: null, message: undefined })
|
||||
})
|
||||
|
||||
it("finds match with string array", () => {
|
||||
const deniedCommands = ["rm", "sudo", "chmod"]
|
||||
const result = findLongestDeniedMatch("rm -rf /", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "rm", message: undefined })
|
||||
})
|
||||
|
||||
it("finds match with DeniedCommand objects", () => {
|
||||
const deniedCommands = [
|
||||
{ prefix: "rm", message: "File deletion is not allowed" },
|
||||
{ prefix: "sudo", message: "Elevated privileges are forbidden" },
|
||||
"chmod",
|
||||
]
|
||||
const result = findLongestDeniedMatch("rm -rf /", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "rm", message: "File deletion is not allowed" })
|
||||
})
|
||||
|
||||
it("returns custom message when available", () => {
|
||||
const deniedCommands = [{ prefix: "sudo", message: "No admin access allowed!" }]
|
||||
const result = findLongestDeniedMatch("sudo apt-get update", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "sudo", message: "No admin access allowed!" })
|
||||
})
|
||||
|
||||
it("returns undefined message for string entries", () => {
|
||||
const deniedCommands = ["sudo"]
|
||||
const result = findLongestDeniedMatch("sudo apt-get update", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "sudo", message: undefined })
|
||||
})
|
||||
|
||||
it("finds longest matching prefix", () => {
|
||||
const deniedCommands = [
|
||||
{ prefix: "git", message: "Git commands restricted" },
|
||||
{ prefix: "git push", message: "Pushing to remote is forbidden" },
|
||||
"git pull",
|
||||
]
|
||||
const result = findLongestDeniedMatch("git push origin main", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "git push", message: "Pushing to remote is forbidden" })
|
||||
})
|
||||
|
||||
it("handles case insensitive matching", () => {
|
||||
const deniedCommands = [{ prefix: "RM", message: "No file deletion" }]
|
||||
const result = findLongestDeniedMatch("rm -rf /", deniedCommands)
|
||||
expect(result).toEqual({ prefix: "rm", message: "No file deletion" })
|
||||
})
|
||||
|
||||
it("handles empty command", () => {
|
||||
const deniedCommands = [{ prefix: "rm" }]
|
||||
const result = findLongestDeniedMatch("", deniedCommands)
|
||||
expect(result).toEqual({ prefix: null, message: undefined })
|
||||
})
|
||||
|
||||
it("handles empty denied commands", () => {
|
||||
const result = findLongestDeniedMatch("rm -rf /", [])
|
||||
expect(result).toEqual({ prefix: null, message: undefined })
|
||||
})
|
||||
|
||||
it("handles undefined denied commands", () => {
|
||||
const result = findLongestDeniedMatch("rm -rf /", undefined as any)
|
||||
expect(result).toEqual({ prefix: null, message: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
describe("Integration with existing functions", () => {
|
||||
it("getCommandDecision works with DeniedCommand objects", () => {
|
||||
const allowedCommands = ["echo", "ls"]
|
||||
const deniedCommands = [{ prefix: "rm", message: "File deletion is dangerous" }, "sudo"]
|
||||
|
||||
// Should auto-deny with custom message available
|
||||
expect(getCommandDecision("rm -rf /", allowedCommands, deniedCommands)).toBe("auto_deny")
|
||||
|
||||
// Should auto-deny without custom message
|
||||
expect(getCommandDecision("sudo apt-get", allowedCommands, deniedCommands)).toBe("auto_deny")
|
||||
|
||||
// Should auto-approve allowed commands
|
||||
expect(getCommandDecision("echo hello", allowedCommands, deniedCommands)).toBe("auto_approve")
|
||||
})
|
||||
|
||||
it("CommandValidator works with DeniedCommand objects", () => {
|
||||
const validator = new CommandValidator(
|
||||
["npm", "echo"],
|
||||
[
|
||||
{ prefix: "rm", message: "Deletion not allowed" },
|
||||
{ prefix: "sudo", message: "Admin access forbidden" },
|
||||
"chmod",
|
||||
],
|
||||
)
|
||||
|
||||
expect(validator.validateCommand("rm file.txt")).toBe("auto_deny")
|
||||
expect(validator.validateCommand("sudo command")).toBe("auto_deny")
|
||||
expect(validator.validateCommand("chmod +x file")).toBe("auto_deny")
|
||||
expect(validator.validateCommand("npm install")).toBe("auto_approve")
|
||||
})
|
||||
|
||||
it("CommandValidator getValidationDetails includes denied command info", () => {
|
||||
const validator = new CommandValidator(["npm"], [{ prefix: "rm", message: "Custom denial message" }])
|
||||
|
||||
const details = validator.getValidationDetails("rm -rf /")
|
||||
expect(details.decision).toBe("auto_deny")
|
||||
expect(details.deniedMatches[0]).toEqual({
|
||||
command: "rm -rf /",
|
||||
match: "rm",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { parse } from "shell-quote"
|
||||
import type { DeniedCommand } from "@roo-code/types"
|
||||
|
||||
type ShellToken = string | { op: string } | { command: string }
|
||||
|
||||
|
|
@ -380,6 +381,52 @@ export function findLongestPrefixMatch(command: string, prefixes: string[]): str
|
|||
return longestMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the longest matching denied command (with optional custom message) from a list of denied commands.
|
||||
*
|
||||
* @param command - The command to match against
|
||||
* @param deniedCommands - List of denied commands (string or object format)
|
||||
* @returns The matching denied command object with prefix and optional message, or null if no match found
|
||||
*/
|
||||
export function findLongestDeniedMatch(
|
||||
command: string,
|
||||
deniedCommands: DeniedCommand[],
|
||||
): { prefix: string | null; message?: string } {
|
||||
if (!command || !deniedCommands?.length) return { prefix: null, message: undefined }
|
||||
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
let longestMatch: { prefix: string; message?: string } | null = null
|
||||
|
||||
for (const denied of deniedCommands) {
|
||||
const prefix = typeof denied === "string" ? denied : denied.prefix
|
||||
const lowerPrefix = prefix.toLowerCase()
|
||||
|
||||
// Handle wildcard "*" - it matches any command
|
||||
if (lowerPrefix === "*" || trimmedCommand.startsWith(lowerPrefix)) {
|
||||
if (!longestMatch || lowerPrefix.length > longestMatch.prefix.length) {
|
||||
longestMatch = {
|
||||
prefix: lowerPrefix,
|
||||
message: typeof denied === "object" ? denied.message : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longestMatch || { prefix: null, message: undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of DeniedCommand objects to a list of prefix strings.
|
||||
* This is used for backward compatibility with existing code.
|
||||
*
|
||||
* @param deniedCommands - List of denied commands (string or object format)
|
||||
* @returns List of prefix strings
|
||||
*/
|
||||
export function deniedCommandsToPrefixes(deniedCommands: DeniedCommand[]): string[] {
|
||||
if (!deniedCommands) return []
|
||||
return deniedCommands.map((cmd) => (typeof cmd === "string" ? cmd : cmd.prefix))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a single command should be auto-approved.
|
||||
* Returns true only for commands that explicitly match the allowlist
|
||||
|
|
@ -508,10 +555,13 @@ export type CommandDecision = "auto_approve" | "auto_deny" | "ask_user"
|
|||
export function getCommandDecision(
|
||||
command: string,
|
||||
allowedCommands: string[],
|
||||
deniedCommands?: string[],
|
||||
deniedCommands?: DeniedCommand[],
|
||||
): CommandDecision {
|
||||
if (!command?.trim()) return "auto_approve"
|
||||
|
||||
// Convert DeniedCommand[] to string[] for backward compatibility
|
||||
const deniedPrefixes = deniedCommands ? deniedCommandsToPrefixes(deniedCommands) : undefined
|
||||
|
||||
// Parse into sub-commands (split by &&, ||, ;, |)
|
||||
const subCommands = parseCommand(command)
|
||||
|
||||
|
|
@ -520,7 +570,7 @@ export function getCommandDecision(
|
|||
// Remove simple PowerShell-like redirections (e.g. 2>&1) before checking
|
||||
const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim()
|
||||
|
||||
return getSingleCommandDecision(cmdWithoutRedirection, allowedCommands, deniedCommands)
|
||||
return getSingleCommandDecision(cmdWithoutRedirection, allowedCommands, deniedPrefixes)
|
||||
})
|
||||
|
||||
// If any sub-command is denied, deny the whole command
|
||||
|
|
@ -622,13 +672,13 @@ export function getSingleCommandDecision(
|
|||
export class CommandValidator {
|
||||
constructor(
|
||||
private allowedCommands: string[],
|
||||
private deniedCommands?: string[],
|
||||
private deniedCommands?: DeniedCommand[],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Update the command lists used for validation
|
||||
*/
|
||||
updateCommandLists(allowedCommands: string[], deniedCommands?: string[]) {
|
||||
updateCommandLists(allowedCommands: string[], deniedCommands?: DeniedCommand[]) {
|
||||
this.allowedCommands = allowedCommands
|
||||
this.deniedCommands = deniedCommands
|
||||
}
|
||||
|
|
@ -693,7 +743,10 @@ export class CommandValidator {
|
|||
|
||||
const deniedMatches = subCommands.map((cmd) => ({
|
||||
command: cmd,
|
||||
match: findLongestPrefixMatch(cmd.replace(/\d*>&\d*/, "").trim(), this.deniedCommands || []),
|
||||
match: findLongestPrefixMatch(
|
||||
cmd.replace(/\d*>&\d*/, "").trim(),
|
||||
deniedCommandsToPrefixes(this.deniedCommands || []),
|
||||
),
|
||||
}))
|
||||
|
||||
return {
|
||||
|
|
@ -741,6 +794,6 @@ export class CommandValidator {
|
|||
* Factory function to create a CommandValidator instance
|
||||
* This is the recommended way to create validators in the application
|
||||
*/
|
||||
export function createCommandValidator(allowedCommands: string[], deniedCommands?: string[]): CommandValidator {
|
||||
export function createCommandValidator(allowedCommands: string[], deniedCommands?: DeniedCommand[]): CommandValidator {
|
||||
return new CommandValidator(allowedCommands, deniedCommands)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue