Skip to content

SEP-1715: Reserve every synthesized execution field name against frontmatter parameter collisions - #1290

Open
marcuscruz-percona wants to merge 11 commits into
mainfrom
SEP-1715
Open

SEP-1715: Reserve every synthesized execution field name against frontmatter parameter collisions#1290
marcuscruz-percona wants to merge 11 commits into
mainfrom
SEP-1715

Conversation

@marcuscruz-percona

@marcuscruz-percona marcuscruz-percona commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • A frontmatter parameter named executor_host, sudo or script_preview made a snippet or script fail to render its execution form at all: the synthesized AppSchema carried two fields with that wire name and validation aborted with duplicate field name(s) across form sections, naming neither the parameter nor the frontmatter. SEP-1664 fixed this for the fourth such name, extra_args, and left the other three unguarded. All four now share a single definition site in the new import-free leaf app/sep/apps/field_names.py, and the frontmatter parameter validator rejects any name in RESERVED_EXECUTION_FIELD_NAMES — so reserving a fifth synthesized name is a one-site edit that cannot be forgotten at the validator.
  • Reservation is unconditional: sudo is rejected on a sudo: never snippet, and script_preview in the disk-script app that never synthesizes that field.
  • Dipper now refuses to dispatch a script with invalid frontmatter parameters. It read validated_parameters to build its form but never checked can_execute, so a script with a dropped parameter still ran — without the argument its author declared. The guard sits in build_dipper_meta_from_args, which both the JSON and legacy form flows funnel through, matching the snippets and disk-script apps. The refusal enumerates the parser's own messages, because Dipper does not surface them on the form: a detail naming only the script would leave the cause invisible everywhere in that app's UI.

Tested

  • Write a snippet whose frontmatter declares - name: sudo, load the snippets detail page, and confirm it renders with a flashed error naming sudo as reserved and the execute button disabled — rather than a 500 from the schema build.
  • Repeat with executor_host and script_preview; confirm the same error and no duplicate-field crash.
  • Set SNIPPETS__META__IGNORE_INVALID_PARAMETERS=true, reload the same snippet, and confirm the form renders with exactly one executor_host field and the execute button enabled.
  • Run a Dipper collection against a service from the UI and confirm it still dispatches normally.
  • Drop a collector script declaring - name: sudo into the Dipper script directory, dispatch it, and confirm the 400 names both the script and the reserved-name validation message.
  • Confirm an existing snippet with ordinary parameter names is unaffected.

Checklist

  • New/modified functions have type hints and rST docstrings
  • New tests added for new features or bug fixes
  • All tests pass locally (make test) — 9316 passed, 421 skipped
  • Pre-commit hooks pass (make run-pre-commit)
  • Database migrations generated if models changed (make makemigrations) — N/A, no model changes
  • User-facing changes documented (README, inline help, UI text)
  • Configuration changes documented with examples — N/A
  • Changelog fragment added under changelog.d/

Notes for reviewers

Behaviour change. No frontmatter in the repository declares any of the four names, so nothing in-tree breaks. A customer artifact that does moves from opaque render crash to parameter dropped, not executable, error explains why — strictly better, but visible, hence the changelog fragment. Separately, the Dipper guard fires on any invalid frontmatter parameter, not just reserved names; all six shipped collectors report can_execute=True, errors=[], so no in-tree Dipper flow is newly blocked.

Not a privilege-escalation fix. Author parameters get opaque generated field names via create_model, so only the fixed-name synthesized fields could collide. The failure was form-schema construction, not argument double-binding.

Reservation is exact-match and case-sensitive. _validate_unique_field_names_in_forms compares with == into a set[str], so Sudo provably cannot collide, and the name pattern permits uppercase today — case-insensitive reservation would smuggle an unrelated breaking change into a bugfix.

Known residual, narrowed but not closed (out of scope on the ticket): Dipper and the disk-script app still do not surface validated_parameters.errors on their forms, so a reserved-name parameter is dropped there with nothing on the page to explain it. Dipper's dispatch refusal now names the offending parameter and why, so the operator who tries to execute learns the cause; the disk-script app's refusal still names only the script. Pre-existing for extra_args. Only the snippets routes flash these messages at render time.

Minor asymmetry left alone: EXECUTION_HOST_LABEL still re-exports via framework.schema while the wire names now come from the leaf, so Dipper reaches two leaf modules by two routes. Repointing it has no acceptance-criteria value and costs three extra import lines.

Error message scope: the rejection message enumerates the full reserved set, slightly beyond AC-5's "names the offending parameter". Kept deliberately — it saves the author a round trip when picking a replacement name.

Changelog app name corrected. The fragment credited the Dipper guard to Collect Diagnostic Data. That is ATW, which is unchanged here — Dipper's plugin schema happens to share that display name, while its app display name is Dipper Data Collection. The fragment now names Dipper.

Test-support cleanup in the last two commits. The three reserved-name suites each repeated the same three assertions plus a near-verbatim paragraph explaining why the whole field set is asserted rather than only the reserved name's absence; both now live in assert_only_synthesized_fields() beside the traversal helpers they already shared. Also: :param:/:return: on the new helpers, signature annotations on the new test methods (pre-existing unannotated methods in those files left alone), raising=False dropped from the two IGNORE_INVALID_PARAMETERS patches so a renamed setting cannot silently pass the test, and one spelling of "synthesize" in field_names.py.

A frontmatter parameter named executor_host, sudo or script_preview made
the execution form fail to build: the synthesized schema carried two
fields with that wire name and AppSchema validation aborted with
"duplicate field name(s) across form sections", naming neither the
parameter nor the frontmatter that produced it. Only the fourth such
name, extra_args, was guarded.

The four names had no shared home. Three lived in framework/schema.py
and one in snippets/models/constants.py, and neither module could import
the other because framework.script_helpers imports snippets.models.
snippet. Introduce app/sep/apps/field_names.py as an import-free leaf
alongside labels.py and nav_icons.py, owning all four constants plus a
RESERVED_EXECUTION_FIELD_NAMES frozenset derived from them, and widen the
SnippetMetaParameter.name validator from scalar equality to set
membership. The builders and the validator now read the same constants,
so reserving a fifth synthesized name is a one-site edit.

Reservation is unconditional: sudo is rejected on a sudo: never snippet
and script_preview in the disk-script app that never synthesizes it, so
an author can apply the rule to the frontmatter in front of them without
knowing which app will render it. Rejection stays graceful - the
parameter is dropped, an error naming it lands on the validated
parameters, and execution is blocked unless invalid parameters are
configured to be ignored.

Also repoint the bare "sudo" literal backing BaseSnippetArgs.sudo_field,
a fifth copy of a reserved name that existed only because the constant
was unreachable across the import cycle.

Coverage generalises from extra_args to all four names, and the two
builders outside the snippets app each gain a case proving a
reserved-name parameter is dropped rather than raising, plus a guard that
every field they synthesize is a member of the reserved set.
Reserving the synthesized execution field names left one acceptance
criterion unmet: a script whose frontmatter declares a reserved name has
the parameter dropped and can_execute set to False, but Dipper never
checked can_execute, so it still dispatched the script without the
argument its author declared. The snippets and disk-script apps already
enforce this at their execution-meta builders.

Guard build_dipper_meta_from_args, which both the JSON and the legacy
form flows funnel through, so one check covers both by construction.

Also strengthen the reservation coverage:

- Restore the synthesized field-type assertions the generalized tests
  dropped. Each schema suite now asserts a name-to-widget mapping, so a
  synthesized field silently retyped fails as loudly as one dropped.
- Derive the reserved-set expectation in test_field_names from the
  module's own constants. A fifth constant added without being added to
  the frozenset now fails; the retyped expectation stayed green.
- Promote the form-field traversal, duplicated across three suites, into
  tests/app/sep/form_schema_utils.py.
- Fix the BaseSnippetArgs docstring to open imperatively and use
  double-backtick literals, and correct the field_names module docstring,
  which claimed every app synthesizes every field.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents frontmatter parameter names from colliding with synthesized execution-form field names by centralizing the wire-name vocabulary (executor_host, sudo, script_preview, extra_args) and rejecting those names during parameter validation. It also tightens Dipper’s execution path so scripts with invalid frontmatter parameters cannot be dispatched unless the operator explicitly opts into ignoring invalid parameters.

Changes:

  • Introduce app/sep/apps/field_names.py as the single, import-free source of truth for synthesized execution field wire names and the reserved-name set.
  • Update snippet/disk-script/Dipper builders and models to use the centralized constants and to reject reserved frontmatter parameter names at parse time.
  • Add/expand test suites across snippets, disk-script source, and Dipper to assert reserved-name behavior and the “block execution on invalid frontmatter” guard; add a changelog fragment.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/app/sep/snippets/test_schema.py Expands schema synthesis tests to cover all reserved names and “ignore invalid parameters” behavior.
tests/app/sep/snippets/models/test_snippet.py Parameterizes reserved-name validation and adds execution-blocking assertions.
tests/app/sep/snippets/models/test_meta.py Extends SnippetMetaParameter validation tests to cover all reserved names and near-misses.
tests/app/sep/form_schema_utils.py Adds shared helpers for asserting emitted form field names/types across schema builders.
tests/app/sep/apps/test_field_names.py Adds tests pinning wire spellings, reserved set completeness, and “leaf module has no imports”.
tests/app/sep/apps/shared/test_disk_script_source.py Updates disk-script schema tests to use shared form helpers and cover reserved-name dropping.
tests/app/sep/apps/dipper/test_schema.py Adds Dipper schema tests ensuring reserved-name parameters are dropped and synthesized fields are reserved.
tests/app/sep/apps/dipper/test_deps.py Adds tests asserting invalid frontmatter blocks Dipper execution meta building unless opted out.
changelog.d/SEP-1715.fixed.md Documents user-visible behavior change and the new Dipper dispatch guard.
app/sep/snippets/schema.py Switches to importing synthesized field names from app.sep.apps.field_names.
app/sep/snippets/models/snippet.py Uses centralized constants (and updates args-model docstring) for execution arg field names.
app/sep/snippets/models/meta.py Generalizes reserved-name rejection to the full reserved set with an improved error message.
app/sep/snippets/models/constants.py Removes the old leaf constant module (replaced by app.sep.apps.field_names).
app/sep/apps/shared/disk_script_source.py Switches synthesized field-name imports to app.sep.apps.field_names.
app/sep/apps/framework/schema.py Removes embedded synthesized field-name constants (now owned by field_names.py).
app/sep/apps/field_names.py New import-free vocabulary module defining synthesized wire names and reserved-name set.
app/sep/apps/dipper/schema.py Switches synthesized field-name imports to app.sep.apps.field_names.
app/sep/apps/dipper/deps.py Adds guard blocking execution meta building when the script has invalid frontmatter parameters.
app/sep/apps/atw/batch.py Switches synthesized field-name imports (and extra-args field name) to app.sep.apps.field_names.

Comment thread tests/app/sep/form_schema_utils.py
@marcuscruz-percona marcuscruz-percona added the qa in progress Someone is currently testing this PR - do not merge it label Aug 5, 2026
The snippets, Dipper and disk-script reserved-name tests each carried the
same three assertions plus a near-verbatim paragraph explaining why the
whole field set is asserted rather than just the reserved name's absence.
Move both into assert_only_synthesized_fields() alongside the traversal
helpers they already share, so the rationale has one home and a fourth
builder's suite gets it for free. Bare asserts in that module need the
same per-file S ignore contract_suite.py already carries.

Annotate and document what the reserved-name work added: the four new
helpers get :param:/:return: entries, and the new test methods get
signature annotations. Pre-existing unannotated methods in these files
are left alone. Drop raising=False from the two IGNORE_INVALID_PARAMETERS
patches, which would have let the tests pass against a renamed setting,
make the per-module synthesized-field maps consistently private, and
settle field_names.py on one spelling of "synthesize".
The dispatch guard fires on can_execute, which is false for any parameter
validation error -- a malformed constraint or a stale visible_when
reference, not only a reserved name. Dipper does not surface
validated_parameters.errors on its form, so the operator saw a refusal
naming the script and nothing else: no screen in that app told them which
parameter was at fault. Enumerate the parser's own messages in the detail
so the refusal is self-explaining wherever it is read.

The changelog fragment credited the guard to Collect Diagnostic Data,
which shares a display name with Dipper's plugin schema but is a
different, unchanged app. Name the app whose behaviour changed.
Review asked the helper to fail loudly on a repeated wire name rather than
let the dict keep whichever field came last. It cannot reach that state:
AppSchema validates uniqueness across form sections in a mode="after"
validator, so a builder emitting one name twice raises before any schema
object exists to traverse -- the repeat surfaces out of the builder call,
not out of an assertion here. Note that where the reader will look for it,
so the next reviewer does not re-derive it.
@marcuscruz-percona marcuscruz-percona added qa passed Tests for this PR are completed and successful. and removed qa in progress Someone is currently testing this PR - do not merge it labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  app/sep
  inventory.py
  app/sep/apps
  field_names.py
  app/sep/apps/atw
  batch.py
  app/sep/apps/dipper
  deps.py
  schema.py
  app/sep/apps/framework
  schema.py
  app/sep/apps/shared
  disk_script_source.py
  app/sep/snippets
  schema.py
  app/sep/snippets/models
  meta.py
  snippet.py
  app/sep/sync/syncers
  pmm.py
Project Total  

This report was generated by python-coverage-comment-action

@marcuscruz-percona

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: marcuscruz-percona <272857389+marcuscruz-percona@users.noreply.github.com>

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts and pushed the fix in commit f4cd6dbf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python qa passed Tests for this PR are completed and successful.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants