# Warden security clearance. # # Grants or revokes a "security clearance" review on PRs based on blocking # findings from the Warden skills: all security findings plus high desktop↔den # sync findings. Advisory sync findings never withhold clearance. Humans still # review and merge; the clearance only satisfies the required-review gate in # the branch ruleset for clean diffs. # # Why workflow_run: this file always executes from the default branch, so a # PR cannot tamper with the clearance logic. The verdict step additionally # refuses to self-clear any PR that touches review machinery # (.github/, warden.toml, .warden/, .agents/skills/, .claude/skills/). # # Identity: a dedicated GitHub App (WARDEN_APP_ID / WARDEN_PRIVATE_KEY in the # `warden-clearance` environment). The app never pushes commits, so # require_last_push_approval always holds (clearance identity != pusher != # author). Until the app secrets are configured this workflow no-ops. name: Warden Clearance on: workflow_run: workflows: [Warden] types: [completed] permissions: actions: read contents: read pull-requests: read concurrency: group: warden-clearance-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false jobs: clearance: if: >- github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 10 environment: warden-clearance steps: - name: Check clearance identity is configured id: identity env: APP_ID: ${{ secrets.WARDEN_APP_ID }} run: | if [ -z "$APP_ID" ]; then echo "WARDEN_APP_ID is not configured; clearance disabled." echo "configured=false" >> "$GITHUB_OUTPUT" else echo "configured=true" >> "$GITHUB_OUTPUT" fi - name: Download clearance summary if: steps.identity.outputs.configured == 'true' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: warden-summary run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} - name: Evaluate verdict if: steps.identity.outputs.configured == 'true' id: verdict env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: | set -euo pipefail verdict() { echo "verdict=$1" >> "$GITHUB_OUTPUT"; exit 0; } summary_sha="$(jq -r '.head_sha' warden-summary.json)" blocking="$(jq -r '.blocking_count // .findings_count' warden-summary.json)" advisory="$(jq -r '.sync_advisory_count // 0' warden-summary.json)" echo "advisory=$advisory" >> "$GITHUB_OUTPUT" if [ "$summary_sha" != "$HEAD_SHA" ]; then echo "Summary SHA ($summary_sha) does not match analyzed SHA ($HEAD_SHA); skipping." verdict skip fi pr="$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" --jq '.[0].number // empty')" if [ -z "$pr" ]; then echo "No PR associated with $HEAD_SHA; skipping." verdict skip fi echo "pr=$pr" >> "$GITHUB_OUTPUT" current_head="$(gh api "repos/$REPO/pulls/$pr" --jq '.head.sha')" if [ "$current_head" != "$HEAD_SHA" ]; then echo "PR #$pr head moved to $current_head; skipping stale run." verdict skip fi # Never self-clear a PR that touches the review machinery. guarded="$(gh api --paginate "repos/$REPO/pulls/$pr/files" --jq '.[].filename' \ | grep -E '^(\.github/|warden\.toml$|\.warden/|\.agents/skills/|\.claude/skills/)' || true)" if [ -n "$guarded" ]; then echo "PR #$pr touches review machinery; human review required:" echo "$guarded" verdict guarded fi if [ "$blocking" = "0" ]; then echo "No blocking findings for $HEAD_SHA." verdict clear else echo "$blocking blocking finding(s) on $HEAD_SHA." verdict flagged fi - name: Mint clearance token if: steps.verdict.outputs.verdict == 'clear' || steps.verdict.outputs.verdict == 'flagged' id: app uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: app-id: ${{ secrets.WARDEN_APP_ID }} private-key: ${{ secrets.WARDEN_PRIVATE_KEY }} - name: Grant clearance if: steps.verdict.outputs.verdict == 'clear' env: GH_TOKEN: ${{ steps.app.outputs.token }} REPO: ${{ github.repository }} PR: ${{ steps.verdict.outputs.pr }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} RUN_URL: ${{ github.event.workflow_run.html_url }} ADVISORY: ${{ steps.verdict.outputs.advisory }} run: | set -euo pipefail BODY="**Warden security clearance: clear.** No blocking findings (security or desktop↔den sync) in this diff (\`$HEAD_SHA\`)." if [[ "$ADVISORY" =~ ^[0-9]+$ ]] && [ "$ADVISORY" -gt 0 ]; then BODY="$BODY $ADVISORY advisory desktop↔den sync note(s) were posted as review comments; they do not affect clearance." fi BODY="$BODY Automated clearance satisfies the required-review gate only — a human still reviews and merges. [Analysis run]($RUN_URL)" gh api "repos/$REPO/pulls/$PR/reviews" \ -f event=APPROVE \ -f commit_id="$HEAD_SHA" \ -f body="$BODY" - name: Revoke stale clearance if: steps.verdict.outputs.verdict == 'flagged' env: GH_TOKEN: ${{ steps.app.outputs.token }} REPO: ${{ github.repository }} PR: ${{ steps.verdict.outputs.pr }} APP_SLUG: ${{ steps.app.outputs.app-slug }} run: | set -euo pipefail rid="$(gh api "repos/$REPO/pulls/$PR/reviews" \ --jq "[.[] | select(.user.login == \"${APP_SLUG}[bot]\" and .state == \"APPROVED\")] | last | .id // empty")" if [ -n "$rid" ]; then gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$rid/dismissals" \ -f message="Warden found new blocking findings (security or desktop↔den sync) on the latest diff; clearance revoked." \ || echo "Could not dismiss review $rid (may already be dismissed)." else echo "No active clearance to revoke." fi