fix: validate plan backup copy schedule and backup window, guard against null backupCopy - #1
Merged
protego-maxima-fianto-duri merged 1 commit intoJul 23, 2026
Conversation
… backupCopy - Reject WEEKLY Backup Copy schedules with no weekdays at request construction - Reject overnight/out-of-range backup window hours instead of silently blocking - Survive plans whose spec.backupCopy is JSON null in list()/get()
protego-maxima-fianto-duri
merged commit Jul 23, 2026
484692f
into
synology-apm:main
4 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
While looking through the protection-plan write path, I came across a few cases
where an unusual or malformed plan configuration is accepted quietly and only
shows up as unexpected behavior later on. This PR adds validation for two of
them and fixes a crash I ran into while listing plans against a live server.
I've tried to keep the changes small and self-contained, and I may well be
missing context on why some of these behave the way they do — so please feel
free to push back on anything, and I'm happy to adjust to match the project's
conventions.
All three were checked against a live APM server, and
make testpasses locally(2999 passed, ruff + mypy clean, coverage 98.76%).
What's changed
1. WEEKLY Backup Copy schedule with no weekdays
ProtectionSchedule's docstring notes that a WEEKLY schedule needs at least oneweekday, and the main plan schedule already checks this in
_validate_plan_schedule_and_retention. The same rule didn't seem to be appliedto
BackupCopyConfig.schedule, so a WEEKLY copy schedule with empty weekdays wasaccepted and sent as
runWeekday: []. In my testing the server stored it as-isand the copy never ran, with no error at creation time.
I added a small
_validate_backup_copy()helper called from bothMachinePlanCreateRequest.__post_init__andM365PlanCreateRequest.__post_init__.AFTER_BACKUP stays valid for Backup Copy, so only the WEEKLY-weekday rule applies.
Both
Raises:docstrings and the two MCP tool descriptions are updated to match.2. Overnight / out-of-range backup window hours
A window like
mon:20-8(intended as 8pm–8am) expands torange(20, 9), whichis empty, so that weekday quietly becomes fully blocked. Out-of-range hours such
as
mon:30-40are similarly dropped. This PR rejects both with aValueError(the overnight message suggests splitting into two ranges, e.g.
20-23,0-8)instead of silently producing a no-backup window.
The check runs at construction time (
MachineBackupWindow.__post_init__) and atthe MCP parse layer, with the request builder kept as a last-resort guard. It
only applies when
enabled=True, to preserve the existing "allowed_hours isignored when disabled" behavior. If you'd rather handle overnight windows
differently (e.g. accept and split them automatically), I'm glad to revisit —
rejecting felt like the safer default, but I wasn't sure of the intended
semantics.
3. Null
backupCopywhen parsing plansapm.plans.list()/get()raisedAttributeErrorwhen a plan'sspec.backupCopywas JSONnull(rather than absent), because.get("backupCopy", {})returnsNonein that case. I hit this on a live serverthat happened to have such plans. Both sites now use
.get("backupCopy") or {},treating null the same as "no backup copy". This should leave the existing
{"enabled": false}behavior unchanged.Testing
its corresponding fix.
input before it reaches the server, a valid plan (WEEKLY copy with weekdays plus
a normal window) is still accepted, and
plans.list()succeeds against planswith a null backupCopy.
make testpasses end to end.Scope
I kept this focused on the three items above. A couple of related things I left
out on purpose to keep the PR small — happy to follow up separately if they'd be
useful:
when used directly via the SDK (the MCP layer already restricts these via a
Literal).
(e.g.
retention), but I only actually observedbackupCopyas null, so Ididn't want to touch paths I couldn't reproduce.
Thanks very much for taking a look — please let me know if you'd prefer any of
this split up, reshaped, or dropped.