Skip to content

fix(workflows): validate requires keys and reject phantom permissions gate#3079

Open
zied-jlassi wants to merge 2 commits into
github:mainfrom
zied-jlassi:fix/workflow-requires-validation
Open

fix(workflows): validate requires keys and reject phantom permissions gate#3079
zied-jlassi wants to merge 2 commits into
github:mainfrom
zied-jlassi:fix/workflow-requires-validation

Conversation

@zied-jlassi

Copy link
Copy Markdown

Description

A workflow's requires: block was parsed (WorkflowDefinition.requires) but its keys were never validated, so a typo or an unsupported key was silently ignored.

Most importantly, an author could write:

requires:
  permissions:
    shell: true

expecting a runtime capability gate — but no such gate exists. A shell step always runs with the user's privileges, so this declaration gives a false sense of sandboxing. (This came up in #2440, where it was understandably assumed that such a declaration was already enforced.)

This PR makes validate_workflow honest about requires:

  • Only the recognised keys are accepted: speckit_version, integrations, tools, mcp.
  • Any unknown key is rejected (a typo surfaces at validation time instead of being silently dropped) — same spirit as the recent "fail loudly on unknown" hardening.
  • requires.permissions is rejected with an explicit message pointing authors at a gate step for approval, so nobody mistakes it for a security boundary.

It does not add any per-step permission system or runtime prompt — requires stays advisory. The model comment and the docs (docs/reference/workflows.md, workflows/PUBLISHING.md) are updated to say so plainly.

Testing

  • Ran existing tests with pytest (full suite green, no regressions)
  • Tested locally with uv run specify --help
  • Added tests: valid recognised keys, non-mapping requires, unknown key, and requires.permissions

Validation is reached on the user-facing paths (workflow add / info / run), so the new errors actually surface.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

AI assistance (an AI coding agent) was used for the initial code review that surfaced the issue, and to help draft the implementation, tests, and documentation wording. All changes were reviewed and verified by me (red→green tests, full suite, ruff) before submitting.

… gate

A workflow's `requires` block was parsed but its keys were never
validated, so a typo or an unsupported key was silently ignored. Most
importantly, authors could write `requires.permissions.shell: true`
expecting a runtime capability gate — but no such gate exists: a `shell`
step always runs with the user's privileges. The declaration gave a
false sense of sandboxing.

`validate_workflow` now accepts only the recognised keys
(`speckit_version`, `integrations`, `tools`, `mcp`) and rejects anything
else, with an explicit error for `requires.permissions` pointing authors
to `gate` steps for approval. Docs and the model comment are updated to
state that `requires` is advisory, not a security boundary.

- Reject non-mapping `requires`, unknown keys, and `requires.permissions`
- Clarify workflows reference + PUBLISHING.md shell-step guidance
- Tests for valid keys, non-mapping, unknown key, and permissions

Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Assisted-by: AI
@zied-jlassi zied-jlassi requested a review from mnriem as a code owner June 21, 2026 21:35
@mnriem mnriem requested a review from Copilot June 22, 2026 13:56

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Tightens workflow validation so requires: only accepts known keys and explicitly rejects requires.permissions to avoid implying a non-existent runtime permission gate.

Changes:

  • Add validation for requires type and allowed keys; reject requires.permissions with a clear error.
  • Add tests covering valid/invalid requires cases (unknown keys, non-mapping, permissions).
  • Update docs to clarify shell runs with user privileges and requires is advisory.
Show a summary per file
File Description
workflows/PUBLISHING.md Documents shell privilege model and that requires is advisory (no permissions gate).
tests/test_workflows.py Adds tests for requires validation behavior (recognized keys, non-mapping, unknown key, permissions).
src/specify_cli/workflows/engine.py Implements requires key/type validation and explicit rejection of requires.permissions.
docs/reference/workflows.md Adds a security note about shell steps and (intended) advisory nature of requires.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 4

Comment thread src/specify_cli/workflows/engine.py Outdated
Comment on lines +201 to +206
if definition.requires:
if not isinstance(definition.requires, dict):
errors.append(
"'requires' must be a mapping (or omitted)."
)
else:
Comment thread docs/reference/workflows.md Outdated
| `fan-out` | Dispatch a step for each item in a list |
| `fan-in` | Aggregate results from a fan-out step |

> **Security note:** a `shell` step runs a local command with **your** privileges. There is no capability sandbox — `requires` (e.g. `requires.permissions`) is an advisory pre-condition block, not a runtime gate, so it does **not** restrict what a step can do. Review any catalog or downloaded workflow before running it, and use a `gate` step to require explicit approval before sensitive or destructive shell commands.
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment on lines +99 to +101
_RECOGNISED_REQUIRES_KEYS = frozenset(
{"speckit_version", "integrations", "tools", "mcp"}
)
Comment thread tests/test_workflows.py Outdated
Comment on lines +2125 to +2126
errors = validate_workflow(definition)
assert any("permissions" in e and "not" in e.lower() for e in errors)

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback

Follow-up to the review on github#3079:

- Guard `requires` validation on `is not None` instead of truthiness so a
  falsy non-mapping value (e.g. `requires: []` or `requires: ''`) is
  reported as an error instead of being silently skipped; `requires:`
  (YAML null) is still treated as an omitted block. Add a regression test.
- Reword the workflows security note so `requires.permissions` is shown
  as rejected/unsupported rather than as a valid example of `requires`.
- Standardize on US spelling (`_RECOGNIZED_REQUIRES_KEYS`, "recognized")
  to match the surrounding code and ease searching.
- Tighten the permissions-rejection test to assert on specific message
  markers (`requires.permissions` and the `gate` guidance) so it fails if
  the validation path or wording drifts.

Assisted-by: AI
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants