From 888e334e7c025267ce3f388c9fb6ee2044e10392 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:49:59 +0900 Subject: [PATCH 1/4] ci(competition): register the engine gate on the default branch pull_request_target workflows only trigger when the workflow file exists on the default branch. the engine gate lived only on the ladder and feature branches, so engine-lane pr #567 got no scoring run at all while the kit gate (registered from main) ran fine. the file is byte-identical to the ladder copy; the run itself still checks out and executes base-branch code per its security model. --- .github/workflows/koth-engine-gate.yml | 152 +++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 .github/workflows/koth-engine-gate.yml diff --git a/.github/workflows/koth-engine-gate.yml b/.github/workflows/koth-engine-gate.yml new file mode 100644 index 00000000..6eb8a56f --- /dev/null +++ b/.github/workflows/koth-engine-gate.yml @@ -0,0 +1,152 @@ +# koth engine gate - scores strategy (ranking-code) submissions. +# +# the deliberate contrast with koth-gate.yml (the kit lane): a kit is data +# and auto-merges; a STRATEGY is code and NEVER auto-merges. this job has no +# write token, holds no secrets, and merges nothing. it scores the submission +# in a sandbox and posts a scorecard + leaderboard note; a human reviews the +# code and merges it as a new default if it wins. that human review is the +# review gate applied to engine code. +# +# security model: +# - pull_request_target: the workflow, the grader (score_strategy.py), and +# the champion strategy all come from the BASE branch. only the challenger +# .py is read from the PR, and it is executed ONLY inside the sandbox +# child (vouch.strategy.run_sandboxed: rlimits + an audit hook blocking +# network/subprocess/writes). +# - permissions: contents: read only. even a full sandbox escape lands on +# an ephemeral runner with a read-only token and no secrets, and cannot +# merge itself. +name: koth-engine-gate + +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: koth-engine-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + gate: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + pull-requests: write # scorecard comment only - no merge + steps: + - name: checkout base branch (trusted code only) + uses: actions/checkout@v4 + + - name: classify the PR + id: classify + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ + --paginate --jq '.[].filename' > /tmp/changed.txt + count=$(wc -l < /tmp/changed.txt) + only=$(head -1 /tmp/changed.txt) + case "$only" in + contrib/strategies/baseline.py|contrib/strategies/README.md) only="" ;; + esac + if [ "$count" = "1" ] && \ + printf '%s' "$only" | grep -qE '^contrib/strategies/[A-Za-z0-9_]+\.py$'; then + echo "mode=engine" >> "$GITHUB_OUTPUT" + echo "path=$only" >> "$GITHUB_OUTPUT" + else + echo "mode=normal" >> "$GITHUB_OUTPUT" + fi + + - name: pass through (not a strategy PR) + if: steps.classify.outputs.mode == 'normal' + run: echo "not a single-strategy PR - engine gate does not apply." + + - name: fetch challenger strategy from the PR (as data) + if: steps.classify.outputs.mode == 'engine' + env: + GH_TOKEN: ${{ github.token }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + KIT_PATH: ${{ steps.classify.outputs.path }} + run: | + gh api "repos/${HEAD_REPO}/contents/${KIT_PATH}?ref=${HEAD_SHA}" \ + > /tmp/strat-meta.json + encoding=$(jq -r '.encoding // ""' /tmp/strat-meta.json) + if [ "$encoding" != "base64" ]; then + echo "strategy not returned as an inlined blob (encoding=$encoding)" >&2 + exit 1 + fi + jq -r '.content' /tmp/strat-meta.json | base64 -d > /tmp/challenger.py + if [ ! -s /tmp/challenger.py ]; then + echo "fetched strategy is empty" >&2 + exit 1 + fi + + - name: set up python + if: steps.classify.outputs.mode == 'engine' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: install vouch (base branch code) + if: steps.classify.outputs.mode == 'engine' + run: python -m pip install -e . + + - name: paired scoring - challenger vs baseline champion (sandboxed) + if: steps.classify.outputs.mode == 'engine' + id: score + env: + BASE_SHA: ${{ github.sha }} + run: | + set +e + python .github/scripts/score_strategy.py \ + --champion contrib/strategies/baseline.py \ + --challenger /tmp/challenger.py \ + --base-sha "$BASE_SHA" \ + --out /tmp/engine-report.json + code=$? + set -e + if [ "$code" = "0" ]; then + echo "verdict=dethroned" >> "$GITHUB_OUTPUT" + elif [ "$code" = "3" ]; then + echo "verdict=held" >> "$GITHUB_OUTPUT" + else + exit "$code" + fi + + - name: post the scorecard + if: steps.classify.outputs.mode == 'engine' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + VERDICT: ${{ steps.score.outputs.verdict }} + run: | + { + echo "koth engine lane - ${VERDICT}" + echo + echo '```json' + cat /tmp/engine-report.json + echo '```' + echo + echo "engine code is NOT auto-merged. a winning strategy earns the" + echo "leaderboard place; a maintainer reviews the code and merges it" + echo "as a new default. the daily result is provisional (public seeds)" + echo "- payout rank is settled by the monthly sealed run." + } > /tmp/comment.md + gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ + --body-file /tmp/comment.md + + - name: report the verdict as the check result + if: steps.classify.outputs.mode == 'engine' + env: + VERDICT: ${{ steps.score.outputs.verdict }} + run: | + echo "verdict: ${VERDICT}" + # a held challenger is a green, informational result - nothing merges + # here regardless, so the gate never blocks. a scoring error already + # failed the job above. + exit 0 From 7bf513cbe371459545bb7c6ae501286201f8ac19 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:53:26 +0900 Subject: [PATCH 2/4] ci(competition): zizmor annotations and shellcheck note for the gates workflow lint only runs on prs that touch workflows, so the koth gates' pull_request_target findings never surfaced until the engine-gate registration pr woke the linter. both gates now carry the ignore annotation with the justification inline (base-branch code only, pr content handled as data, read-only tokens), and the scorecard comment script declares its literal backticks for shellcheck. --- .github/workflows/koth-engine-gate.yml | 2 +- .github/workflows/koth-gate.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/koth-engine-gate.yml b/.github/workflows/koth-engine-gate.yml index 6eb8a56f..e3f6fc9e 100644 --- a/.github/workflows/koth-engine-gate.yml +++ b/.github/workflows/koth-engine-gate.yml @@ -19,7 +19,7 @@ name: koth-engine-gate on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] workflow + grader + champion all come from the base branch; the PR's .py runs only inside the sandbox child, and the job token is read-only with no secrets types: [opened, synchronize, reopened, ready_for_review] concurrency: diff --git a/.github/workflows/koth-gate.yml b/.github/workflows/koth-gate.yml index c089c8f7..db3181bb 100644 --- a/.github/workflows/koth-gate.yml +++ b/.github/workflows/koth-gate.yml @@ -17,7 +17,7 @@ name: koth-gate on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] no PR code ever executes: the kit is fetched as data, validated against a closed-world allowlist, and scored by base-branch code with a read-only checkout types: [opened, synchronize, reopened, ready_for_review] concurrency: @@ -138,6 +138,7 @@ jobs: BASE_REF: ${{ github.event.pull_request.base.ref }} LADDER_BASE: ${{ vars.KOTH_LADDER_BASE }} run: | + # shellcheck disable=SC2016 # the posted markdown carries literal backticks if [ "$BASE_REF" = "$LADDER_BASE" ] && [ -n "$LADDER_BASE" ]; then gate_note="auto-merge on dethrone (provisional ladder branch)" else From 9c3c908bf98e36fae8ec58b5298dcc6003c65554 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:55:41 +0900 Subject: [PATCH 3/4] ci(competition): bare shellcheck directive directly above the block a trailing comment on the directive line keeps shellcheck from parsing it, and a top-of-script placement did not attach to the compound command. the directive now sits bare, immediately above the comment block it covers. --- .github/workflows/koth-gate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/koth-gate.yml b/.github/workflows/koth-gate.yml index db3181bb..548d4605 100644 --- a/.github/workflows/koth-gate.yml +++ b/.github/workflows/koth-gate.yml @@ -138,12 +138,13 @@ jobs: BASE_REF: ${{ github.event.pull_request.base.ref }} LADDER_BASE: ${{ vars.KOTH_LADDER_BASE }} run: | - # shellcheck disable=SC2016 # the posted markdown carries literal backticks if [ "$BASE_REF" = "$LADDER_BASE" ] && [ -n "$LADDER_BASE" ]; then gate_note="auto-merge on dethrone (provisional ladder branch)" else gate_note="scored only - auto-merge is disabled for base '${BASE_REF}'; a maintainer promotes proven champions to shipped defaults by hand" fi + # the posted markdown carries literal backticks + # shellcheck disable=SC2016 { echo "koth ladder - ${VERDICT}" echo From d1f73aede9187f519a1b126f0ea18e66f5758a2f Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:58:19 +0900 Subject: [PATCH 4/4] ci(competition): engine gate review fixes - sha pins and delete-only prs pin checkout and setup-python to immutable commit shas (mutable tags in a pull_request_target workflow are a supply-chain surface), and classify a delete-only strategy pr as normal instead of engine - the filename shape matched but there is nothing to fetch at the head sha, so the gate failed messy instead of passing through. --- .github/workflows/koth-engine-gate.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/koth-engine-gate.yml b/.github/workflows/koth-engine-gate.yml index e3f6fc9e..6230bbf3 100644 --- a/.github/workflows/koth-engine-gate.yml +++ b/.github/workflows/koth-engine-gate.yml @@ -38,7 +38,7 @@ jobs: pull-requests: write # scorecard comment only - no merge steps: - name: checkout base branch (trusted code only) - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: classify the PR id: classify @@ -48,8 +48,13 @@ jobs: run: | gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ --paginate --jq '.[].filename' > /tmp/changed.txt + # a delete-only pr matches the filename shape but has nothing to + # fetch at the head sha - classify it normal, not engine. + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ + --paginate --jq '.[] | select(.status != "removed") | .filename' \ + > /tmp/present.txt count=$(wc -l < /tmp/changed.txt) - only=$(head -1 /tmp/changed.txt) + only=$(head -1 /tmp/present.txt) case "$only" in contrib/strategies/baseline.py|contrib/strategies/README.md) only="" ;; esac @@ -88,7 +93,7 @@ jobs: - name: set up python if: steps.classify.outputs.mode == 'engine' - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12'