feat(config): add command pattern detection tool in exec settings (#1971)

* Add command pattern testing endpoint and UI tool

Adds a new API endpoint `/api/config/test-command-patterns` that tests a
command against configured whitelist and blacklist patterns, along with
a frontend UI component to interactively test patterns.

* Only process deny patterns when enableDenyPatterns is true
This commit is contained in:
柚子
2026-03-25 10:19:20 +08:00
committed by GitHub
parent 8da0638ee3
commit adf1a5749d
5 changed files with 343 additions and 0 deletions
@@ -1,3 +1,4 @@
import { useState } from "react"
import type { ReactNode } from "react"
import { useTranslation } from "react-i18next"
@@ -7,6 +8,7 @@ import {
type LauncherForm,
} from "@/components/config/form-model"
import { Field, SwitchCardField } from "@/components/shared-form"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
@@ -201,6 +203,56 @@ interface ExecSectionProps {
export function ExecSection({ form, onFieldChange }: ExecSectionProps) {
const { t } = useTranslation()
const [testCommand, setTestCommand] = useState("")
const [testResult, setTestResult] = useState<{
allowed: boolean
blocked: boolean
matchedWhitelist: string | null
matchedBlacklist: string | null
} | null>(null)
const [isLoading, setIsLoading] = useState(false)
const testPatterns = async () => {
if (!testCommand.trim()) {
setTestResult(null)
return
}
const allowPatterns = form.customAllowPatternsText
.split("\n")
.map((p) => p.trim())
.filter((p) => p.length > 0)
const denyPatterns = form.enableDenyPatterns
? form.customDenyPatternsText
.split("\n")
.map((p) => p.trim())
.filter((p) => p.length > 0)
: []
setIsLoading(true)
try {
const res = await fetch("/api/config/test-command-patterns", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
allow_patterns: allowPatterns,
deny_patterns: denyPatterns,
command: testCommand,
}),
})
const data = await res.json()
setTestResult({
allowed: data.allowed,
blocked: data.blocked,
matchedWhitelist: data.matched_whitelist ?? null,
matchedBlacklist: data.matched_blacklist ?? null,
})
} catch {
setTestResult(null)
} finally {
setIsLoading(false)
}
}
return (
<ConfigSectionCard title={t("pages.config.sections.exec")}>
@@ -266,6 +318,50 @@ export function ExecSection({ form, onFieldChange }: ExecSectionProps) {
/>
</Field>
<Field
label={t("pages.config.pattern_detector_title")}
hint={t("pages.config.pattern_detector_hint")}
layout="setting-row"
controlClassName="md:max-w-md"
>
<div className="flex w-full flex-col gap-2">
<div className="flex gap-2">
<Input
value={testCommand}
placeholder={t(
"pages.config.pattern_detector_input_placeholder",
)}
onChange={(e) => setTestCommand(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
testPatterns()
}
}}
/>
<Button onClick={testPatterns} disabled={isLoading}>
{t("pages.config.pattern_detector_test_button")}
</Button>
</div>
{testResult && (
<div
className={`rounded-md p-2 text-sm ${
testResult.allowed
? "bg-green-500/10 text-green-600"
: testResult.blocked
? "bg-red-500/10 text-red-600"
: "bg-muted text-muted-foreground"
}`}
>
{testResult.allowed
? `${t("pages.config.pattern_detector_result_allowed")}${testResult.matchedWhitelist ? ` (${testResult.matchedWhitelist})` : ""}`
: testResult.blocked
? `${t("pages.config.pattern_detector_result_blocked")}${testResult.matchedBlacklist ? ` (${testResult.matchedBlacklist})` : ""}`
: t("pages.config.pattern_detector_result_no_match")}
</div>
)}
</div>
</Field>
<Field
label={t("pages.config.exec_timeout_seconds")}
hint={t("pages.config.exec_timeout_seconds_hint")}