-
Notifications
You must be signed in to change notification settings - Fork 66
213 lines (200 loc) · 9.26 KB
/
Copy pathkoth-engine-gate.yml
File metadata and controls
213 lines (200 loc) · 9.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# koth engine gate - scores strategy (ranking-code) submissions and, on a
# dethrone against the ladder branch, arms auto-merge: the benchmark decides
# what enters the quarantined contrib lane, with no human in the loop.
#
# what keeps that sane - the containment contract, all three parts:
# 1. merged strategies are QUARANTINED code: everything that executes a
# contrib strategy (both gates, the local bench loop) runs it through
# the sandbox child (vouch.strategy.run_sandboxed). nothing imports
# contrib code in-process, and the ledger/ratchet scripts treat the
# winner as text (promote_champion.py copies bytes, imports nothing).
# 2. the scoring job never holds a write token. it executes the untrusted
# challenger, so it gets contents: read + the comment scope only; the
# arm job below holds the write scopes, checks out nothing, and runs
# no challenger code - it turns the verdict into a merge, that is all.
# accepted residual: a sandbox escape during scoring can forge the
# verdict and land its own PR in contrib/ - inside the quarantine.
# 3. shipped defaults stay human. auto-merge fires only when the PR base
# is the ladder branch (KOTH_LADDER_BASE); promotion of a champion
# into src/vouch as trusted, importable default code remains a
# human-reviewed PR. the benchmark is never the sole gate to code
# users install.
#
# security model of the scoring job itself (unchanged):
# - 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).
name: koth-engine-gate
on:
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:
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 - the merge lives in `arm`
outputs:
mode: ${{ steps.classify.outputs.mode }}
verdict: ${{ steps.score.outputs.verdict }}
steps:
- name: checkout base branch (trusted code only)
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# pull_request_target defaults to the DEFAULT branch, not the PR
# base - pin the base ref explicitly (same fix the kit gate got).
# still trusted code: a branch of this repo, never the PR head.
with:
ref: ${{ github.event.pull_request.base.ref }}
- 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
# 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/present.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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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
run: |
set +e
# seed identity = the tree actually scored (the checked-out base
# tip), not github.sha, which is the default branch under
# pull_request_target
BASE_SHA="$(git rev-parse HEAD)"
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 }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
LADDER_BASE: ${{ vars.KOTH_LADDER_BASE }}
run: |
if [ "$BASE_REF" = "$LADDER_BASE" ] && [ -n "$LADDER_BASE" ]; then
gate_note="auto-merge on dethrone (quarantined contrib lane: merged strategies only ever run inside the sandbox; shipped defaults still require a human PR)"
else
gate_note="scored only - auto-merge is disabled for base '${BASE_REF}'; a maintainer reviews and merges winners here"
fi
{
echo "koth engine lane - ${VERDICT}"
echo
echo '```json'
cat /tmp/engine-report.json
echo '```'
echo
echo "${gate_note}."
echo
echo "the daily result is provisional (public seeds) - payout rank"
echo "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, and the arm
# job merges nothing for it. a scoring error already failed the
# job above.
exit 0
# the write-capable half, deliberately separated from the job that
# executes the untrusted challenger: this job checks out nothing, runs no
# challenger code, and touches no file from the PR - it reads the gate's
# verdict and arms native auto-merge, exactly like the kit lane. the
# merge itself is performed by github once required checks are green.
# auto-merge fires ONLY when the PR base is the dedicated ladder branch
# (repo variable KOTH_LADDER_BASE): the trunk keeps human review, and a
# champion reaches shipped defaults only through a human PR.
arm:
needs: gate
if: >-
needs.gate.outputs.mode == 'engine' &&
needs.gate.outputs.verdict == 'dethroned' &&
vars.KOTH_LADDER_BASE != '' &&
github.event.pull_request.base.ref == vars.KOTH_LADDER_BASE
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # auto-merge (squash) needs it
pull-requests: write # enable auto-merge
steps:
- name: enable auto-merge on dethrone (ladder branch only)
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--auto --squash