mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user