fix(workflows): reject a non-integer or out-of-range current_step_index in RunState resume - #4325
Noor-ul-ain001 wants to merge 4 commits into
Conversation
…oad() RunState.load() shape-checks every other persisted field on resume -- workflow_id, installed_workflow_id, installed_registry_root, and inputs -- raising a clean "Invalid run state: ..." ValueError on a malformed value. current_step_index was the one field passed through unchecked. resume() later slices `definition.steps[state.current_step_index :]` with no guard of its own, so a non-int value (e.g. a hand-edited or externally-written state.json) reaches that slice and raises a raw `TypeError: slice indices must be integers or None or have an __index__ method` from deep inside resume() instead. A negative value slices from the end instead of failing, silently resuming from the wrong step. This mirrors the sibling field-validation pattern in RunState.load() (e.g. the workflow_id/installed_workflow_id checks) and the recurring "validate cleanly vs. crash at runtime" bug class already fixed across this codebase for step configs (e.g. github#4144, github#3899). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW9fAYsCBCAgdKWovtSyqt
There was a problem hiding this comment.
🟢 Approval recommended
The validation is correct and well covered; only minor inaccurate explanatory wording remains.
Pull request overview
Adds validation for persisted workflow resume indices to prevent malformed state from causing runtime errors or incorrect resumption.
Changes:
- Rejects non-integer, boolean, and negative step indices.
- Adds parameterized regression coverage.
File summaries
| File | Description |
|---|---|
src/specify_cli/workflows/engine.py |
Validates current_step_index during state loading. |
tests/test_workflows.py |
Tests malformed index values. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # ``resume()`` slices ``definition.steps[state.current_step_index :]`` | ||
| # with no guard of its own -- unlike ``workflow_id`` / | ||
| # ``installed_workflow_id`` / ``installed_registry_root`` / ``inputs`` | ||
| # above, this field was never shape-checked here. A non-int value (a | ||
| # hand-edited or externally-written state.json, e.g. a string or | ||
| # float) reaches that slice and raises a raw, unhelpful | ||
| # ``TypeError: slice indices must be integers or None or have an | ||
| # __index__ method`` from deep inside ``resume()`` instead of the | ||
| # clean "Invalid run state: ..." this loader already gives every | ||
| # other malformed field. A negative value slices from the end instead | ||
| # of failing, silently resuming from the wrong step. Reject both here, | ||
| # consistent with the sibling checks. ``bool`` is an ``int`` subclass, | ||
| # so it is excluded explicitly (mirrors the ``max_iterations`` / | ||
| # ``continue_on_error`` bool guards elsewhere in this module). |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Out-of-range positive indices can still silently skip all remaining steps and mark the run completed.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
RunState.load() checks current_step_index is a non-negative int but can't bound it against the step count, which is only known once the workflow definition is loaded in resume(). An out-of-range positive index (e.g. a hand-edited state.json) reached resume()'s definition.steps[state.current_step_index:] slice, which for any index >= len(steps) is an empty list -- so the run silently completed having executed no steps, instead of failing like every other malformed state field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U74yBbvVQCPwB7Ed8Dzeu6
|
The out-of-range-index review request is addressed. We’ll keep this as Please narrow the remaining claim that every other restored field is validated; several fields are still restored directly. This does not call for expanding the PR into a loader-wide validation audit. Please also add a plain AI-disclosure statement covering the Claude assistance and Copilot Autofix contribution, including the tool/model, mode/settings, and extent of assistance. Drafted for @mnriem with assistance from GitHub Copilot (model: GPT-6 Astra; interactive comment drafting). |
The comment added for the current_step_index guard implied this loader shape-checks every field it restores. It doesn't: step_results, workflow_dir, current_step_id, created_at, updated_at, and error are all still assigned directly from state_data with no isinstance check. Only workflow_id, installed_workflow_id, installed_registry_root, and inputs are validated (pre-existing), plus current_step_index as of this PR. Reworded to name only those fields rather than claiming universal coverage. No behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks! Addressed both remaining points:
Let me know if anything else needs adjusting. |
Summary
RunState.load()restorescurrent_step_indexviastate_data.get("current_step_index", 0)with no shape check, unlike a few of the other fields it restores —workflow_id,installed_workflow_id,installed_registry_root, andinputs— which are validated and raise a cleanInvalid run state: ...ValueErroron a malformed value. (This loader does not shape-check every restored field:step_results,workflow_dir,current_step_id,created_at/updated_at, anderrorare still assigned directly with noisinstancecheck. This PR only addressescurrent_step_index, since it's the oneresume()depends on for a safe list slice — it is not a loader-wide validation audit.)resume()later slicesdefinition.steps[state.current_step_index :]with no guard of its own, so a non-intcurrent_step_index(e.g. a hand-edited or externally-writtenstate.json) reaches that slice and raises a rawTypeError: slice indices must be integers or None or have an __index__ method, instead of the same clean domain error the other validated fields already get. A negative value slices from the end of the step list instead of failing, silently resuming from the wrong step.resume()itself (RunState.load()can reject non-int/negative values but can't check the upper bound — the step count isn't known until the workflow definition loads, later, inresume()). Without this, an out-of-range index would silently slice to an empty list and complete the run without executing any step.casesblock on switch steps #4144 for switch'scases, fix(workflows): reject mismatched run state IDs #3899 for run-state IDs) — here it shows up inRunState.load()'s field validation instead of a step'svalidate()/execute()pair.Test plan
test_load_rejects_invalid_current_step_index(parametrized over a string, float, negative int, list, dict, and bool) totests/test_workflows.py::TestRunStatetest_resume_rejects_out_of_range_current_step_indexcovering the upper-bound case caught inresume()src/specify_cli/workflows/engine.py, confirmed the failure, then restored it and confirmed the pass)tests/test_workflows.py::TestRunState— passing, no regressionstests/test_workflows.py— pre-existing Windows-only symlink-guard/tmp-dir failures reproduced identically onmain, unrelated to this changeAI Disclosure
This PR was authored with Claude Code (model: Claude Sonnet 5), used in its interactive, agentic CLI mode with full local tool access (file read/edit, running
pytest,git). It identified the bug, wrote the source fix, the out-of-range follow-up fix, and the regression tests, and ran the verification described above, under my direction. I reviewed the diff and test output myself before opening this PR and understand the change being made.Separately, GitHub Copilot reviewed this PR and left two inline findings pointing out that the new comment/docstring overclaimed loader-wide field validation (the same issue raised in review here). Its automated Copilot Autofix commit (
bf9de58b, "Potential fix for pull request finding") addressed one of the two — trimming the inaccurate claim out of the test docstring intests/test_workflows.py, no logic change — which I reviewed before it stayed on the branch. The matching finding on theengine.pysource comment was not auto-fixed; that one is what this update (commite61c1561) resolves.Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com