Summary
The command step in the workflow engine accepts an input: field that is expected to be a mapping. However, a workflow that sets input: null (or omits the value entirely — YAML shorthand for null) crashes at execution with a cryptic AttributeError: 'NoneType' object has no attribute 'items', and validation does not catch it.
This is the same class of type-validation gap that has been getting cleaned up in recent commits — e.g. #3327 (shell timeout), #3270 (loop cap ignores bool max_iterations), #3448 (membership test on non-iterable). Reporting so the command step gets the same treatment.
Reproduction
Minimal repro against main (0.12.13.dev0):
from specify_cli.workflows.steps.command import CommandStep
from specify_cli.workflows.base import StepContext
step = CommandStep()
ctx = StepContext(project_root="/tmp")
step.execute(
{"id": "test", "command": "/speckit.specify", "input": None},
ctx,
)
Output:
AttributeError: 'NoneType' object has no attribute 'items'
The offending line is src/specify_cli/workflows/steps/command/__init__.py:37:
input_data = config.get("input", {})
# ...
for key, value in input_data.items(): # <-- crashes when input is null / list / str
resolved_input[key] = evaluate_expression(value, context)
CommandStep.validate() (same file, line ~152) only checks that command is present — nothing for input. So WorkflowEngine.validate() returns no errors and the crash escapes at run time instead of surfacing as a clean validation error at load time.
The natural YAML that triggers this looks harmless:
- id: draft
type: command
command: /speckit.specify
input: # <-- YAML parses this as null
Also crashes on input: "some string" and input: [1, 2] for the same reason (.items() on a non-mapping).
Suggested fix
Match the pattern already applied in the shell / while-loop / do-while validators:
- In
validate(): reject non-mapping input (allow absence) with a clear error, e.g. input must be a mapping, got <type>.
- In
execute(): defensively coerce input_data to {} when it is not a mapping, so a caller that bypasses WorkflowEngine.validate() still gets a clean skip rather than a crash.
Happy to open a PR — I have a fix + tests ready.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. The reproducer was executed against a fresh clone of main and the fix is covered by new unit tests in tests/test_workflows.py.
Summary
The
commandstep in the workflow engine accepts aninput:field that is expected to be a mapping. However, a workflow that setsinput: null(or omits the value entirely — YAML shorthand fornull) crashes at execution with a crypticAttributeError: 'NoneType' object has no attribute 'items', and validation does not catch it.This is the same class of type-validation gap that has been getting cleaned up in recent commits — e.g. #3327 (shell timeout), #3270 (loop cap ignores
bool max_iterations), #3448 (membership test on non-iterable). Reporting so thecommandstep gets the same treatment.Reproduction
Minimal repro against
main(0.12.13.dev0):Output:
The offending line is
src/specify_cli/workflows/steps/command/__init__.py:37:CommandStep.validate()(same file, line ~152) only checks thatcommandis present — nothing forinput. SoWorkflowEngine.validate()returns no errors and the crash escapes at run time instead of surfacing as a clean validation error at load time.The natural YAML that triggers this looks harmless:
Also crashes on
input: "some string"andinput: [1, 2]for the same reason (.items()on a non-mapping).Suggested fix
Match the pattern already applied in the shell / while-loop / do-while validators:
validate(): reject non-mappinginput(allow absence) with a clear error, e.g.input must be a mapping, got <type>.execute(): defensively coerceinput_datato{}when it is not a mapping, so a caller that bypassesWorkflowEngine.validate()still gets a clean skip rather than a crash.Happy to open a PR — I have a fix + tests ready.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. The reproducer was executed against a fresh clone of
mainand the fix is covered by new unit tests intests/test_workflows.py.