Hold an automation's schedule to the sensors it was checked against - #2536
Conversation
An automation's output sensors are checked against its creator's permissions when the automation is created, which is the only moment a user is present. They are predicted from the fields that name them, and a sensor the prediction misses is not left unchecked so much as checked for the wrong thing: it is read as an input, so read access is asked for where recording data calls for create-children access. A schedule job created by an automation is now held to that prediction, and refuses to record on any other sensor. Where there is nothing to hold the job to, it proceeds as before: a job that is not an automation's, an automation deleted since the job was queued, or one whose sensors cannot be determined, which is logged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS6V8nZyRzMxsvqGnNpUTk Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS6V8nZyRzMxsvqGnNpUTk Signed-off-by: F.N. Claessen <claessen@seita.nl>
Documentation build overview
12 files changed ·
|
Main gained the automations CRUD work (#2294), which rewrote the comment above OUTPUT_SENSOR_FIELDS and added tests to the same file this branch appends to. Kept main's account of what the list is, and this branch's account of what now holds a schedule job to it; both files' new tests sit side by side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS6V8nZyRzMxsvqGnNpUTk Signed-off-by: F.N. Claessen <claessen@seita.nl>
`resolve_schedule_automation_sensors` handed the scheduler a flex config that had already been through `AssetTriggerSchema`, so `collect_flex_config` — which merges the asset tree's config with the message's, reading sensors by id — met `Sensor` objects where it expects ids and raised a `ProgrammingError` no caller catches. A data generator deserializes its own config, so the scheduler now receives the config as it was written and does that itself. The trigger message is loaded for its timing and its asset only. A database error joins what is reported as unknown sensors, that being what it means here. With resolution reliable, the guard fails closed: an automation whose sensors cannot be determined records nothing, rather than proceeding unchecked. The refusal test no longer stubs the permitted set, so it derives it the way production does. Closes the blocker recorded on #2421. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS6V8nZyRzMxsvqGnNpUTk Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness and consistency issues (potential partial writes before refusal, missing DB rollback on SQLAlchemy errors, and a changelog PR-number mismatch) that should be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR tightens scheduling automation safety by binding what an automation job may write at run time to the set of output sensors that were permission-checked at automation creation, preventing “silent downgrades” where a missed output field would only require read access.
Changes:
- Add a run-time guard in
make_scheduleto refuse scheduler results that target sensors outside the automation’s cleared output-sensor set. - Fix schedule-automation sensor resolution by passing the original (non-deserialized) flex config to the scheduler and handling DB errors as “sensors unknown”.
- Add fresh-db tests covering refusal of undeclared sensors and “held to nothing / held to nothing-permits-nothing” scenarios, plus a main changelog entry.
File summaries
| File | Description |
|---|---|
| flexmeasures/data/services/scheduling.py | Introduces the runtime “unchecked sensor” guard and helper to derive permitted output sensors for automation-triggered jobs |
| flexmeasures/data/services/automations.py | Adjusts output-sensor field commentary and fixes schedule automation sensor resolution (including catching DB errors) |
| flexmeasures/data/tests/test_automations_fresh_db.py | Adds regression tests for refusing undeclared sensors and behavior when jobs aren’t tied to an automation / sensors are unknown |
| documentation/changelog.rst | Adds a bugfix changelog entry describing the new refusal behavior |
Review details
Suppressed comments (1)
flexmeasures/data/services/scheduling.py:1092
- The unchecked-sensor guard currently raises inside the result-writing loop, so if the scheduler returns multiple outputs you can end up writing beliefs for earlier sensors before failing on a later unchecked sensor. To ensure the refusal happens before any database writes, pre-validate all result sensors once before iterating (and keep/optionally remove the per-result check as a safety net).
permitted_output_sensor_ids = _sensors_this_job_may_record_on(rq_job)
scheduling_result_dict: dict = SchedulingJobResult().to_dict()
num_beliefs_created = 0
for result in consumption_schedule:
if result.get("name") == SCHEDULING_RESULT_KEY:
- Files reviewed: 4/4 changed files
- Comments generated: 6
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The guard raised from inside the writing loop, so a scheduler returning a permitted sensor before an unchecked one would have written the first. The job's transaction would have rolled that back, `save_to_db` only flushing, but a refusal should not rest on the caller's transaction discipline, and `make_schedule` is called directly too. Every sensor the scheduler returned is now judged before any of them is written, and the message names them all. A `SQLAlchemyError` now rolls the session back before it is reported as unknown sensors: the session is unusable until it is, and the caller goes on to render a response through it. The changelog entry also read "PR #2524" while linking to this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🔵 Needs a closer look
New/updated docstrings and comments violate the repo’s “line breaks only after punctuation” convention and should be adjusted before approval.
Review details
Suppressed comments (3)
flexmeasures/data/services/scheduling.py:924
- Docstring wraps mid-phrase (line ends without punctuation), which violates the repository convention that docstrings/comments should only break lines after punctuation for stable review/search.
An automation's output sensors are checked against its creator's permissions when the automation is created,
which is the only moment a user is present.
Those sensors are predicted from the fields that name them (see `OUTPUT_SENSOR_FIELDS`),
so a scheduler which returned results for a sensor named by some other field would record data
on a sensor that was only ever checked for read access, as the prediction reads it as an input.
flexmeasures/data/services/automations.py:63
- This comment block wraps mid-phrase (lines end without punctuation), which violates the repository convention to only break comment lines after punctuation for stable review/search.
# Extend it whenever a flex-model or flex-context field starts naming somewhere results are recorded.
# A field this list misses is not left unchecked so much as checked for the wrong thing: the sensor is read
# as an input, so its creator needs only read access where recording data calls for create-children access.
# A schedule job created by an automation is therefore held to the sensors predicted here
# (see `_sensors_this_job_may_record_on`), so that drift shows up as a refusal rather than a quiet downgrade.
flexmeasures/data/tests/test_automations_fresh_db.py:395
- Docstring wraps mid-phrase (line ends without punctuation), which violates the repository convention that docstrings/comments should only break lines after punctuation for stable review/search.
An automation's output sensors are checked against its creator's permissions when it is created,
and those are predicted from the fields that name them.
A sensor the prediction misses is read as an input instead, so it is checked for read access
where recording data calls for create-children access.
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
"Allow using numeric values for flex-model fields accepting dimensionless quantities" read "PR #1144" while linking to pull/1299. The link is the right one: #1299 is "Allow numeric values for dimensionless fields", where #1144 is multi-commitments. Found while fixing the same defect in this PR's own entry. It is the only other one: all three changelogs are now free of link texts that disagree with what they link to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
Automations are unreleased, so the guard is not a fix to anything users have run into. Move its entry from Bugfixes to the per-PR automations section, and word it as a property of the feature rather than as a behaviour that changed. Break the three new comment blocks after punctuation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…-only-on-checked-sensors Signed-off-by: F.N. Claessen <claessen@seita.nl> # Conflicts: # documentation/changelog.rst
There was a problem hiding this comment.
🟢 Approval recommended
The functional changes align with the stated permission-hardening goal and are backed by targeted tests, with only a minor docstring wording correction suggested.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
flexmeasures/data/services/automations.py:395
- The docstring says “Anything else raised while working out the config…” is reported as
AutomationSensorsUnknown, but the implementation only wrapsNotImplementedError,ValueErrorandSQLAlchemyError(other exception types will still propagate). Reword this to match the actual exception handling so future readers don’t assume broader catching than is implemented.
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
The docstring claimed anything raised while working out the config is reported as AutomationSensorsUnknown, where only NotImplementedError, ValueError and SQLAlchemyError are wrapped and the rest propagates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
The same gap #2536 closed for schedules: an automation's output sensors are checked against its creator's permissions when it is created, but a reporter decides at run time which sensors it returns results for, and run_report_job saved whatever came back. A report job created by an automation now refuses the whole set before saving any of it when a result names a sensor outside the ones that were checked, and records no coverage, so the next run starts where the last successful one ended. The helper that works out those sensors moves from the scheduling service to the automations service, as both job types now use it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129WrXeJ5gia2pctFH93BqC Signed-off-by: F.N. Claessen <claessen@seita.nl>
Description
Closes #2421.
An automation's output sensors are checked against its creator's permissions when the automation is created — the only moment a user is present. They are predicted, by matching
OUTPUT_SENSOR_FIELDSagainst the flex-model and flex-context; what a scheduler records on is decided later, by the scheduler. Nothing held the second to the first.A sensor the prediction misses is not left unchecked so much as checked for the wrong thing:
collect_sensorspicks it up as an input, soreadaccess is demanded where recording data calls forcreate-children.This makes the prediction binding.
make_schedulenow works out which sensors an automation-triggered job was cleared for, and raisesScheduleWritesUncheckedSensorbefore writing anything else.Not release-gating, and not an exploit today. An automation's
parameterscannot be edited after creation (AutomationUpdateSchemaaccepts onlyname,cron,timezoneandactive), and the prediction is complete forStorageScheduler— see #2421 for the field-by-field check. What this closes is the drift hazard: the day a new output-naming field is added without extending the tuple, read access silently starts standing in for write access.Where there is nothing to hold the job to, it proceeds as before: a job that is not an automation's, an automation deleted since the job was queued, or one whose sensors cannot be determined — the last logged as an error. Failing closed was considered and rejected: before this guard nothing was checked at run time at all, so proceeding is no worse than the status quo, whereas refusing would stop an automation recording at all, over a risk that is currently theoretical.
The obstacle recorded on #2421 is fixed here
resolve_automation_sensorscould not resolve a schedule automation's sensors in all cases: it handed the scheduler a flex config that had already been throughAssetTriggerSchema, socollect_flex_config()— which merges the asset tree's config with the message's, reading sensors by id — metSensorobjects where it expects ids and raised apsycopg2.ProgrammingErrorno caller catches.A data generator deserializes its own config, as forecasters already do through
_parameters_schema. So the trigger message is now loaded for its timing and its asset only, and the flex config reaches the scheduler as it was written. A database error joins what is reported asAutomationSensorsUnknown, that being what it means here.Two things follow:
How to test
test_an_automations_schedule_refuses_a_sensor_nobody_checked— a scheduler returning an undeclared sensor is refused. Verified to fail (DID NOT RAISE) with the guard removed.test_a_job_that_is_not_an_automations_is_held_to_nothing— API-triggered jobs, and automations that have gone away, are held to nothing.🤖 Generated with Claude Code
https://claude.ai/code/session_01WS6V8nZyRzMxsvqGnNpUTk