diff --git a/.github/workflows/attest.yml b/.github/workflows/attest.yml index 1434e90..a2a11d3 100644 --- a/.github/workflows/attest.yml +++ b/.github/workflows/attest.yml @@ -4,7 +4,8 @@ name: attest # stage 1 pre-fetch the package with scripts disabled (network on, no third-party code runs) # stage 2 capture the declared surface inside a network-less, non-root, read-only sandbox # stage 3 Sigstore-sign the lock on the runner, bound to the package coordinate (v2 statement) -# then open ONE pull request containing only new paths. See docs/SANDBOX.md. +# then land ONE commit containing only new paths on main, behind the append-only gate. +# See docs/SANDBOX.md. # # The signing identity is this workflow at main: # https://github.com/DataScience-EngineeringExperts/mcp-warden-locks/.github/workflows/attest.yml@refs/heads/main @@ -100,7 +101,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - pull-requests: write checks: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -112,7 +112,12 @@ jobs: path: incoming merge-multiple: true - - name: Open one PR with only new paths, then report + arm the gate + # The enterprise setting forbids GITHUB_TOKEN from opening pull requests, so the + # attester lands its commit directly — but ONLY after the same append-only gate a + # human PR gets has passed on that exact commit and been reported as the required + # `append-only` check. Branch protection (required check + enforce_admins, no + # force-push, no deletion) is what admits the fast-forward; nothing else can. + - name: Land new lock paths on main behind the append-only gate env: GH_TOKEN: ${{ github.token }} run: | @@ -123,7 +128,7 @@ jobs: (cd incoming && find . -type f | sed 's#^\./##') | while read -r f; do if [ -e "locks/$f" ]; then echo "skip existing locks/$f"; else mkdir -p "$(dirname "locks/$f")"; cp "incoming/$f" "locks/$f"; fi done - if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard locks)" ]; then echo "nothing new"; exit 0; fi + if [ -z "$(git ls-files --others --exclude-standard locks)" ]; then echo "nothing new"; exit 0; fi day=$(date -u +%Y-%m-%d); run="$GITHUB_RUN_ID" branch="attest/$day-$run" count=$(git ls-files --others --exclude-standard locks | grep -c '\.lock$' || true) @@ -132,15 +137,14 @@ jobs: git checkout -b "$branch" git add locks git commit -q -m "attest: $day ($count new locks)" -m "attester=$ATTESTER_ID warden=$WARDEN_REF run=$run" - git push -u origin "$branch" head=$(git rev-parse HEAD) - # The same gate the pull_request workflow runs — reported as a check-run because - # token-caused events do not start workflows. Fails closed: a violation aborts here. + # Gate first, fail closed: a violation stops here and nothing is pushed anywhere. bash tools/append_only_check.sh origin/main "$head" - pr=$(gh pr create --base main --head "$branch" --title "attest: $day ($count new locks)" \ - --body "Nightly attestation by \`$ATTESTER_ID\` — $count new lock(s), mcp-warden \`$WARDEN_REF\`, run $run. Only new paths; see docs/SANDBOX.md. Consensus attests observation, not safety.") + git push -u origin "$branch" gh api "repos/$GITHUB_REPOSITORY/check-runs" -f name=append-only -f head_sha="$head" \ -f status=completed -f conclusion=success \ -f 'output[title]=append-only OK' -f "output[summary]=$count new lock path(s); no modifications or deletions under locks/ or attesters.json." >/dev/null - gh pr merge --squash --auto "$pr" - echo "opened $pr" + # Fast-forward main. Branch protection admits this only because `append-only` is green on $head. + gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/heads/main" -f sha="$head" -F force=false >/dev/null + git push origin --delete "$branch" + echo "landed $count new lock(s) on main at $head" diff --git a/README.md b/README.md index a122cbd..8b5cbab 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,10 @@ Signature verification needs network to Sigstore's trust root on first use. `.github/workflows/attest.yml` runs nightly: for each target in `targets.yaml` with no lock yet — pre-fetch with install scripts disabled → capture inside a **network-less, non-root, -read-only, capability-dropped** container → sign on the runner with ambient OIDC → one PR -with only new paths, merged automatically once the `append-only` gate passes. The full +read-only, capability-dropped** container → sign on the runner with ambient OIDC → one commit +with only new paths, landed on `main` only after the `append-only` gate has passed on that +exact commit (the org forbids Actions from opening PRs, so the attester fast-forwards `main` +behind the same required check every human PR must pass). The full contract, and what it does *not* defend against, is in [`docs/SANDBOX.md`](docs/SANDBOX.md). Targets are pinned exact versions from the official MCP servers and widely used third-party