Skip to content

fix(ci): shell-free release candidate that keeps the tag out of script text - #59

Merged
hseshadr merged 2 commits into
mainfrom
fix/fleet-policy-findings
Sep 25, 2026
Merged

hseshadr merged 2 commits into
mainfrom
fix/fleet-policy-findings

Conversation

@hseshadr

Copy link
Copy Markdown
Owner

Claim touched: a dispatched release tag can never run code on the runner, and the release candidate follows the fleet's shell-free shape (checkout, Dagger, upload).

Why

The hseshadr/ci fleet scan (scan_fleet(include_central=True), 2026-09-25) reports five findings on main @ 57df579. This PR fixes the three on release-candidate.yml:

Code Path Detail
shell-step .github/workflows/release-candidate.yml run steps are forbidden (the "Validate release tag" step)
shell-step .github/workflows/release-candidate.yml run steps are forbidden (the "Build and verify release candidate" step)
candidate-order .github/workflows/release-candidate.yml expected checkout, Dagger, upload

The tag-injection hardening (7b3ab4d) was right that --tag=${{ inputs.tag }} in dagger-for-github args was injectable. Its fix moved the call into run: steps, and fleet policy forbids those. The same property holds without shell steps.

What

  • One pinned dagger/dagger-for-github step. env: TAG: ${{ inputs.tag }}, and args: release-candidate --tag="$TAG" --commit-sha="$GITHUB_SHA" .... The action pastes args into its own bash script, so bash expands "$TAG" as a single literal word. The value is never parsed as code, and no ${{ }} expression appears in args.
  • The vX.Y.Z regex shell step is dropped. release_candidate in Dagger already rejects a requested tag that differs from the verified candidate's tag (_require_requested_tag, run through EXPECTED_TAG inside the container, not shell text).
  • Contract reversal (tests inverted, not deleted): tests/test_workflow_security.py used to assert the shell shape: a guard step, an install-only action, and a run: release call. It now asserts the policy shape and no run: steps. The bash regex tests are replaced by a stronger test: it expands the real workflow args in bash, the way the action does, with 8 hostile tags ($(touch pwned), backticks, ;, a newline, a quote breakout, an extra flag). It asserts that each tag reaches Dagger as exactly one argument and that no file is created.

This touches only release-candidate.yml, tests/test_workflow_security.py and CHANGELOG.md. The bump PR #58 touches dagger.json, .dagger/src/edgeproc_core/main.py and .dagger/tests/test_public_contracts.py, so the two do not overlap (git merge-tree is clean). They can merge in either order.

Not changed: publish.yml (2 findings, intentionally left)

Code Detail
shell-step the "Verify the candidate's lineage" step (#56)
pypi-shape expected exact source-free PyPI bridge

This step is a real control. It blocks a workflow_dispatch on a tag named main from getting past the head_branch gate. Fleet policy has no compliant way to express it, and the only Dagger identity the policy allows there is the candidate's own code. Removing the step would reopen the hole, so the policy has to change. That is tracked in hseshadr/ci#49.

Evidence

Check Result
RED 11 failed on the old workflow: shape, env/args, and 8 hostile-tag expansions (At index 0 diff: 'None' != 'release-candidate')
GREEN uv run poe gate exit 0: 361 passed, coverage 99.31%
Fleet policy (ci.fleet_policy.validate_workflow) old file: shell-step x2 and candidate-order. New file: no findings
Mutation: --tag=$TAG (unquoted) 7 red
Mutation: --tag=${{ inputs.tag }} (the pre-0.5.0 bug) 10 red
Mutation: add any run: step 2 red

Not verified live: a real workflow_dispatch run. That needs a release tag, so it happens at the next release.

🤖 Generated with Claude Code

https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a

…t text

The hseshadr/ci fleet scan reports shell-step (x2) and candidate-order on
release-candidate.yml: fleet policy allows only checkout, Dagger, upload.

Keep the tag-injection hardening inside the pinned dagger-for-github step
instead of run: steps. The tag reaches the step only as the TAG env var,
and args hold only double-quoted variables, so the action's bash expands
it as one inert word. Dagger already rejects a requested tag that differs
from the verified candidate's tag, so the shell regex guard is dropped.

Tests now expand the real args in bash with hostile tags and assert that
each arrives as one literal argument and runs nothing.

publish.yml is intentionally unchanged: its lineage step is a real
control that fleet policy cannot yet express (see hseshadr/ci).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
@hseshadr
hseshadr merged commit 6afd821 into main Sep 25, 2026
2 checks passed
@hseshadr
hseshadr deleted the fix/fleet-policy-findings branch September 25, 2026 18:09
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.

1 participant