feat(config): add exec controls and gate cron commands on exec settings (#1685)

- add a dedicated exec settings section in the config page
- support timeout and custom allow/deny regex patterns for exec
- validate custom exec regex patterns in the config API
- block cron command scheduling and execution when exec is disabled
- update tests and i18n strings for the new command settings
This commit is contained in:
wenjie
2026-03-17 18:56:52 +08:00
committed by GitHub
parent 8a44410e37
commit 7b9fdaec32
9 changed files with 379 additions and 28 deletions
@@ -16,6 +16,7 @@ import {
AgentDefaultsSection,
CronSection,
DevicesSection,
ExecSection,
LauncherSection,
RuntimeSection,
} from "@/components/config/config-sections"
@@ -27,6 +28,7 @@ import {
buildFormFromConfig,
parseCIDRText,
parseIntField,
parseMultilineList,
} from "@/components/config/form-model"
import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
@@ -170,6 +172,28 @@ export function ConfigPage() {
"Cron exec timeout",
{ min: 0 },
)
const execConfigPatch: Record<string, unknown> = {
enabled: form.execEnabled,
}
if (form.execEnabled) {
execConfigPatch.allow_remote = form.allowRemote
execConfigPatch.enable_deny_patterns = form.enableDenyPatterns
execConfigPatch.custom_allow_patterns = parseMultilineList(
form.customAllowPatternsText,
)
execConfigPatch.timeout_seconds = parseIntField(
form.execTimeoutSeconds,
"Exec timeout",
{ min: 0 },
)
if (form.enableDenyPatterns) {
execConfigPatch.custom_deny_patterns = parseMultilineList(
form.customDenyPatternsText,
)
}
}
await patchAppConfig({
agents: {
@@ -190,9 +214,7 @@ export function ConfigPage() {
allow_command: form.allowCommand,
exec_timeout_minutes: cronExecTimeoutMinutes,
},
exec: {
allow_remote: form.allowRemote,
},
exec: execConfigPatch,
},
heartbeat: {
enabled: form.heartbeatEnabled,
@@ -289,6 +311,8 @@ export function ConfigPage() {
<RuntimeSection form={form} onFieldChange={updateField} />
<ExecSection form={form} onFieldChange={updateField} />
<CronSection form={form} onFieldChange={updateField} />
<LauncherSection
@@ -93,14 +93,6 @@ export function AgentDefaultsSection({
}
/>
<SwitchCardField
label={t("pages.config.allow_remote")}
hint={t("pages.config.allow_remote_hint")}
layout="setting-row"
checked={form.allowRemote}
onCheckedChange={(checked) => onFieldChange("allowRemote", checked)}
/>
<Field
label={t("pages.config.max_tokens")}
hint={t("pages.config.max_tokens_hint")}
@@ -161,6 +153,98 @@ export function AgentDefaultsSection({
)
}
interface ExecSectionProps {
form: CoreConfigForm
onFieldChange: UpdateCoreField
}
export function ExecSection({ form, onFieldChange }: ExecSectionProps) {
const { t } = useTranslation()
return (
<ConfigSectionCard title={t("pages.config.sections.exec")}>
<SwitchCardField
label={t("pages.config.exec_enabled")}
hint={t("pages.config.exec_enabled_hint")}
layout="setting-row"
checked={form.execEnabled}
onCheckedChange={(checked) => onFieldChange("execEnabled", checked)}
/>
{form.execEnabled && (
<>
<SwitchCardField
label={t("pages.config.allow_remote")}
hint={t("pages.config.allow_remote_hint")}
layout="setting-row"
checked={form.allowRemote}
onCheckedChange={(checked) => onFieldChange("allowRemote", checked)}
/>
<SwitchCardField
label={t("pages.config.enable_deny_patterns")}
hint={t("pages.config.enable_deny_patterns_hint")}
layout="setting-row"
checked={form.enableDenyPatterns}
onCheckedChange={(checked) =>
onFieldChange("enableDenyPatterns", checked)
}
/>
{form.enableDenyPatterns && (
<Field
label={t("pages.config.custom_deny_patterns")}
hint={t("pages.config.custom_deny_patterns_hint")}
layout="setting-row"
controlClassName="md:max-w-md"
>
<Textarea
value={form.customDenyPatternsText}
placeholder={t("pages.config.custom_patterns_placeholder")}
className="min-h-[88px]"
onChange={(e) =>
onFieldChange("customDenyPatternsText", e.target.value)
}
/>
</Field>
)}
<Field
label={t("pages.config.custom_allow_patterns")}
hint={t("pages.config.custom_allow_patterns_hint")}
layout="setting-row"
controlClassName="md:max-w-md"
>
<Textarea
value={form.customAllowPatternsText}
placeholder={t("pages.config.custom_patterns_placeholder")}
className="min-h-[88px]"
onChange={(e) =>
onFieldChange("customAllowPatternsText", e.target.value)
}
/>
</Field>
<Field
label={t("pages.config.exec_timeout_seconds")}
hint={t("pages.config.exec_timeout_seconds_hint")}
layout="setting-row"
>
<Input
type="number"
min={0}
value={form.execTimeoutSeconds}
onChange={(e) =>
onFieldChange("execTimeoutSeconds", e.target.value)
}
/>
</Field>
</>
)}
</ConfigSectionCard>
)
}
interface RuntimeSectionProps {
form: CoreConfigForm
onFieldChange: UpdateCoreField
@@ -251,6 +335,7 @@ export function CronSection({ form, onFieldChange }: CronSectionProps) {
hint={t("pages.config.allow_shell_execution_hint")}
layout="setting-row"
checked={form.allowCommand}
disabled={!form.execEnabled}
onCheckedChange={(checked) => onFieldChange("allowCommand", checked)}
/>
@@ -262,6 +347,7 @@ export function CronSection({ form, onFieldChange }: CronSectionProps) {
<Input
type="number"
min={0}
disabled={!form.execEnabled}
value={form.cronExecTimeoutMinutes}
onChange={(e) =>
onFieldChange("cronExecTimeoutMinutes", e.target.value)
@@ -3,7 +3,12 @@ export type JsonRecord = Record<string, unknown>
export interface CoreConfigForm {
workspace: string
restrictToWorkspace: boolean
execEnabled: boolean
allowRemote: boolean
enableDenyPatterns: boolean
customDenyPatternsText: string
customAllowPatternsText: string
execTimeoutSeconds: string
allowCommand: boolean
cronExecTimeoutMinutes: string
maxTokens: string
@@ -57,7 +62,12 @@ export const DM_SCOPE_OPTIONS = [
export const EMPTY_FORM: CoreConfigForm = {
workspace: "",
restrictToWorkspace: true,
execEnabled: true,
allowRemote: true,
enableDenyPatterns: true,
customDenyPatternsText: "",
customAllowPatternsText: "",
execTimeoutSeconds: "0",
allowCommand: true,
cronExecTimeoutMinutes: "5",
maxTokens: "32768",
@@ -119,10 +129,32 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
defaults.restrict_to_workspace === undefined
? EMPTY_FORM.restrictToWorkspace
: asBool(defaults.restrict_to_workspace),
execEnabled:
exec.enabled === undefined
? EMPTY_FORM.execEnabled
: asBool(exec.enabled),
allowRemote:
exec.allow_remote === undefined
? EMPTY_FORM.allowRemote
: asBool(exec.allow_remote),
enableDenyPatterns:
exec.enable_deny_patterns === undefined
? EMPTY_FORM.enableDenyPatterns
: asBool(exec.enable_deny_patterns),
customDenyPatternsText: Array.isArray(exec.custom_deny_patterns)
? exec.custom_deny_patterns
.filter((value): value is string => typeof value === "string")
.join("\n")
: EMPTY_FORM.customDenyPatternsText,
customAllowPatternsText: Array.isArray(exec.custom_allow_patterns)
? exec.custom_allow_patterns
.filter((value): value is string => typeof value === "string")
.join("\n")
: EMPTY_FORM.customAllowPatternsText,
execTimeoutSeconds: asNumberString(
exec.timeout_seconds,
EMPTY_FORM.execTimeoutSeconds,
),
allowCommand:
cron.allow_command === undefined
? EMPTY_FORM.allowCommand
@@ -191,3 +223,13 @@ export function parseCIDRText(raw: string): string[] {
.map((v) => v.trim())
.filter((v) => v.length > 0)
}
export function parseMultilineList(raw: string): string[] {
if (!raw.trim()) {
return []
}
return raw
.split("\n")
.map((value) => value.trim())
.filter((value) => value.length > 0)
}