fix(frontend): Validate and sanitize cron expressions for scheduler API (#10719)

<!-- Clearly explain the need for these changes: -->

### Need for these changes 💥

This PR resolves Linear issue `SECRT-1290`, addressing a critical bug
where the scheduler API fails with a "Wrong number of fields" error when
empty or invalid cron expressions are submitted from the frontend. This
was causing production errors and a poor user experience. It was an off
by one error

### Changes 🏗️


Fix off by one error + add additional logging / error messaging when
someone makes an invalid cron


https://github.com/user-attachments/assets/775881a9-707b-4c4f-b23a-bd7118a358ee



### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Attempt to schedule an agent with an empty cron expression from
the UI and confirm a frontend toast error.
- [x] Attempt to schedule an agent with an incomplete yearly cron (no
months selected) from the UI and confirm a frontend toast error and UI
warning.
- [x] Attempt to schedule an agent with an incomplete monthly cron (no
days selected) from the UI and confirm a frontend toast error and UI
warning.
- [x] Attempt to schedule an agent with an incomplete weekly cron (no
days selected) from the UI and confirm a frontend toast error and UI
warning.
- [x] Verify that valid cron expressions can still be scheduled
successfully.
  - [x] Run backend unit tests for scheduler cron validation.
  - [x] Run frontend unit tests for cron expression utility.


---
Linear Issue: [SECRT-1290](https://linear.app/autogpt/issue/SECRT-1290)

<a
href="https://cursor.com/background-agent?bcId=bc-8bc10502-9498-4dbd-afa2-93e15990fa8c">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/open-in-cursor-dark.svg">
<source media="(prefers-color-scheme: light)"
srcset="https://cursor.com/open-in-cursor-light.svg">
<img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg">
  </picture>
</a>
<a
href="https://cursor.com/agents?id=bc-8bc10502-9498-4dbd-afa2-93e15990fa8c">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/open-in-web-dark.svg">
<source media="(prefers-color-scheme: light)"
srcset="https://cursor.com/open-in-web-light.svg">
    <img alt="Open in Web" src="https://cursor.com/open-in-web.svg">
  </picture>
</a>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
Nicholas Tindle
2025-08-25 06:16:26 -05:00
committed by GitHub
parent a9530b7304
commit e0520f5e0a
4 changed files with 43 additions and 2 deletions

View File

@@ -39,6 +39,17 @@ export function CronSchedulerDialog({
});
return;
}
// Validate cron expression before proceeding
if (!cronExpression || cronExpression.trim() === "") {
toast({
variant: "destructive",
title: "Invalid schedule",
description: "Please enter a valid cron expression",
});
return;
}
afterCronCreation(cronExpression, scheduleName);
setOpen(false);
};

View File

@@ -213,6 +213,11 @@ export function CronScheduler({
</Button>
))}
</div>
{selectedWeekDays.length === 0 && (
<p className="text-sm text-red-500">
Please select at least one day of the week
</p>
)}
</div>
)}
{frequency === "monthly" && (
@@ -274,6 +279,11 @@ export function CronScheduler({
))}
</div>
)}
{selectedMonthDays.length === 0 && (
<p className="text-sm text-red-500">
Please select at least one day of the month
</p>
)}
</div>
)}
{frequency === "yearly" && (
@@ -287,7 +297,9 @@ export function CronScheduler({
if (selectedMonths.length === months.length) {
setSelectedMonths([]);
} else {
setSelectedMonths(Array.from({ length: 12 }, (_, i) => i));
setSelectedMonths(
Array.from({ length: 12 }, (_, i) => i + 1),
);
}
}}
>
@@ -319,6 +331,11 @@ export function CronScheduler({
);
})}
</div>
{selectedMonths.length === 0 && (
<p className="text-sm text-red-500">
Please select at least one month
</p>
)}
</div>
)}

View File

@@ -872,6 +872,16 @@ export default function useAgentGraph(
) => {
if (!savedAgent || isScheduling) return;
// Validate cron expression
if (!cronExpression || cronExpression.trim() === "") {
toast({
variant: "destructive",
title: "Invalid schedule",
description: "Please enter a valid cron expression",
});
return;
}
setIsScheduling(true);
try {
await api.createGraphExecutionSchedule({
@@ -891,7 +901,7 @@ export default function useAgentGraph(
router.push("/monitoring");
}
} catch (error) {
console.error(error);
console.error("Error scheduling agent:", error);
toast({
variant: "destructive",
title: "Error scheduling agent",

View File

@@ -49,16 +49,19 @@ export function makeCronExpression(params: CronExpressionParams): string {
if (frequency === "daily") return `${params.minute} ${params.hour} * * *`;
if (frequency === "weekly") {
const { minute, hour, days } = params;
if (days.length === 0) return ""; // Return empty string for invalid weekly schedule
const weekDaysExpr = days.sort((a, b) => a - b).join(",");
return `${minute} ${hour} * * ${weekDaysExpr}`;
}
if (frequency === "monthly") {
const { minute, hour, days } = params;
if (days.length === 0) return ""; // Return empty string for invalid monthly schedule
const monthDaysExpr = days.sort((a, b) => a - b).join(",");
return `${minute} ${hour} ${monthDaysExpr} * *`;
}
if (frequency === "yearly") {
const { minute, hour, months } = params;
if (months.length === 0) return ""; // Return empty string for invalid yearly schedule
const monthList = months.sort((a, b) => a - b).join(",");
return `${minute} ${hour} 1 ${monthList} *`;
}