Skip to content

Hold an automation's schedule to the sensors it was checked against - #2536

Merged
Flix6x merged 9 commits into
mainfrom
feat/schedules-record-only-on-checked-sensors
Sep 16, 2026
Merged

Flix6x merged 9 commits into
mainfrom
feat/schedules-record-only-on-checked-sensors

Conversation

@Flix6x

@Flix6x Flix6x commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

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_FIELDS against 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_sensors picks it up as an input, so read access is demanded where recording data calls for create-children.

This makes the prediction binding. make_schedule now works out which sensors an automation-triggered job was cleared for, and raises ScheduleWritesUncheckedSensor before writing anything else.

Not release-gating, and not an exploit today. An automation's parameters cannot be edited after creation (AutomationUpdateSchema accepts only name, cron, timezone and active), and the prediction is complete for StorageScheduler — 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_sensors could not resolve a schedule automation's sensors in all cases: it 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 psycopg2.ProgrammingError no 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 as AutomationSensorsUnknown, that being what it means here.

Two things follow:

  • The guard fails closed. An automation whose sensors cannot be determined records nothing, rather than proceeding unchecked. That was the whole reason the obstacle mattered.
  • The refusal test no longer stubs the permitted set, deriving it the way production does — which is the evidence that the resolver defect is actually gone rather than merely worked around.

How to test

pytest flexmeasures/data/tests/test_automations_fresh_db.py -k "refuses_a_sensor or held_to_nothing"
  • 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

Flix6x and others added 2 commits September 15, 2026 12:01
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>
@read-the-docs-community

read-the-docs-community Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Flix6x and others added 2 commits September 15, 2026 21:26
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>
@Flix6x
Flix6x marked this pull request as ready for review September 16, 2026 10:23
@Flix6x
Flix6x requested a lite review from Copilot September 16, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_schedule to 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.

Comment thread flexmeasures/data/services/automations.py
Comment thread flexmeasures/data/services/scheduling.py
Comment thread documentation/changelog.rst Outdated
Comment thread flexmeasures/data/services/automations.py Outdated
Comment thread flexmeasures/data/services/scheduling.py
Comment thread flexmeasures/data/tests/test_automations_fresh_db.py Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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>
@Flix6x Flix6x added this to the 1.1.0 milestone Sep 16, 2026
@Flix6x Flix6x self-assigned this Sep 16, 2026
Comment thread documentation/changelog.rst Outdated
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 wraps NotImplementedError, ValueError and SQLAlchemyError (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>
@Flix6x
Flix6x merged commit e990c8f into main Sep 16, 2026
13 checks passed
@Flix6x
Flix6x deleted the feat/schedules-record-only-on-checked-sensors branch September 16, 2026 11:57
Flix6x added a commit that referenced this pull request Sep 16, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check the sensors a scheduler actually writes to, instead of predicting them when an automation is created

2 participants