Use a PAT for Models inference so the major-bump AI gate actually runs#15
Conversation
The AI half of the major-bump gate has never produced a verdict: 2/2 real majors (EBA#101 numpy, EBA#105 setup-uv) fell back to manual review because every GitHub Models inference call 403s. Probed from inside Actions on the real GITHUB_TOKEN: the catalogue reads fine (HTTP 200) but inference returns a bare 403 with an empty body for all 8 models tried across every tier (low/high/custom). So it is not a workflow permission, not a model choice, and not a tier -- the org has no Models inference entitlement. A user token gets HTTP 200 on the same call. - Prefer MODEL_TOKEN over GITHUB_TOKEN. The old order checked GITHUB_TOKEN first, which is ALWAYS set in Actions, so a supplied PAT would have been silently ignored and kept 403ing. - Pass MODEL_TOKEN from the MODELS_PAT secret. Unset renders as "" and falls back to GITHUB_TOKEN -> 403 -> verdict=human, i.e. today's behaviour. - Explain a 403 instead of surfacing a bare "PermissionDeniedError". GitHub sends no body, so the name alone cost hours of hunting through permissions. - Correct the docstring, which claimed the built-in token authenticates. Refs #14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Security review of this diffPutting a credential into a
Pre-existing residual risk, not introduced here, worth stating out loud: the changelog we feed the model is untrusted upstream text, so a hostile release note could attempt prompt injection toward |
✅ End-to-end test on the two real major bumps that never got a verdictRan this branch's script, unmodified, with a real PAT in Case 1 — EBA#105, {"verdict": "auto", "risk": "low",
"reason": "There is no explicit indication of breaking changes; updates are maintenance, documentation improvements, dependency bumps, support for a new architecture, and internal changes; no public API or input/output changes described."}Matches reality: a human reviewed and merged it, CI green. This one would have merged itself, still gated on green CI. Case 2 — EBA#101, {"verdict": "human", "risk": "high",
"reason": "NumPy 2.5.0 drops support for Python 3.11, removes distutils, and expires multiple deprecations which may break usage or compatibility for typical consumers, plus renames/removes some APIs; these are breaking changes."}It named the actual root cause, unprompted. #101's CI really did fail because numpy 2.5 needs Python >=3.12 while So the gate discriminates: it waves through the safe major and stops the breaking one for the right reason. Not a rubber stamp. What this proves: What it does not prove: the same call from inside the Actions runner with |
Verifying the AI gate meant waiting for a real major bump to wander in, then reading its comment -- a terrible feedback loop, and it tests on a live PR. This adds an on-demand self-test instead: gh workflow run models-selftest.yaml --repo realrate/.github It probes inference with the built-in token AND the PAT side by side, then runs the real script over two fixtures with known answers (a breaking numpy-style bump -> human, a benign action bump -> auto), and renders a summary table. Notes on its design: - Fails LOUD, unlike the gate (which fails safe to verdict=human). Failing safe is why a dead AI went unnoticed for weeks; a diagnostic must go red instead. - Rejects fallback verdicts, not just malformed ones. The script emits verdict=human on ANY error, which matches the breaking fixture's expected answer -- so a total outage would otherwise PASS. Detected via the fallback's signature (risk=unknown AND reason ending "manual review."). - Model judgement drift warns rather than fails; only a missing/fallback verdict fails, since the model is non-deterministic. - Skips cleanly (green) when MODELS_PAT is unset, so it never blocks PRs while the secret does not exist yet. Also fixes a bug this testing exposed: MODEL_ID defaulted to `openai/gpt-5`, which is reasoning-tier and rejects this script's `max_tokens` with a BadRequestError. The documented default never worked; only the workflow's explicit override did. Default is now `openai/gpt-4.1`, which is verified. Refs #14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Added:
|
| fixture | expected | verdict | risk |
|---|---|---|---|
| breaking (numpy-style: drops Py3.11, removes distutils, ABI change) | human | ✅ human | high |
| safe (action bump, no input/output changes) | auto | ✅ auto | low |
Broken token → exit 1 and red, as shown above. And in this PR's actual CI run, with no secret configured, it did the right thing: detected the absent secret, emitted a notice, skipped the three live steps, and stayed green — so it never blocks PRs before the secret exists.
🐛 Bug this testing exposed
MODEL_ID defaulted to openai/gpt-5, which is reasoning-tier and rejects this script's max_tokens with a BadRequestError. The documented default never worked — only the workflow's explicit override did, which is why it went unnoticed. Now defaults to openai/gpt-4.1, verified working with MODEL_ID unset.
After the secret lands
Run the workflow. Green means majors will start getting real verdicts. If it also reports the built-in token returning 200, the org entitlement got fixed and MODELS_PAT can simply be deleted.
Lets a new PAT be verified under a separate secret name, falling back to MODELS_PAT when unset, so the production secret stays undisturbed during testing. Refs #14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Resolved: a personal-account token does inference from Actions (proven)Self-test run from inside Actions, on
Real script over both fixtures: breaking bump → Conclusion: the Actions runner does not force org-attribution. Inference with a personal PAT is fine. The earlier Last step for production: put that same working token into the real gh secret set MODELS_PAT --org realrate --visibility all # needs org-ownerThen delete the temporary |
Refs #14.
Why
The AI half of the major-bump gate has never produced a verdict in production. Both real majors so far (EBA#101 numpy, EBA#105 setup-uv) were routed to a human because the Models call 403s, not because the AI judged them risky.
What I measured
Probed from inside Actions, in an org repo, on the real
GITHUB_TOKEN(temp branch, since deleted):/catalog/modelsopenai/gpt-4.1)lowtier (gpt-4.1-mini,gpt-4o-mini,phi-4,mistral-small)hightier (gpt-4.1,gpt-4o)customtier (gpt-5-mini,o4-mini)The job log confirms
Models: readis granted. So: auth works and the catalogue reads, but every inference is refused regardless of model or tier. It is not a workflow permission, notpull_request_target, not Dependabot's restricted token, and not the model id. The org simply has no Models inference entitlement, while a user account does.Changes
MODEL_TOKENoverGITHUB_TOKEN. The old order checkedGITHUB_TOKENfirst, and that is always set in Actions — so a supplied PAT would have been silently ignored and kept 403ing. This one-line ordering bug would have made the whole PAT approach look broken.MODEL_TOKENfrom a newMODELS_PATsecret.PermissionDeniedErrortold maintainers nothing and sent us hunting through workflow permissions. It now names the likely cause and links AI risk gate: GitHub Models call returns 403 — major-bump AI verdict never runs #14.Safety
Unchanged, and fails safe. If
MODELS_PATis absent the secret renders as"", the script falls back toGITHUB_TOKEN, gets a 403, and emitsverdict=human— exactly today's behaviour. Any error still routes to a human, andgated_merge.shstill requires the PR's own checks to pass, so no bump can merge on an errored verdict. This PR can land before the secret exists and change nothing until it does.Validation
verdict=human, 403 → actionable message (bystatus_codeand SDK class name), non-403 errors unchanged.test_gated_merge.shALL PASS,shellcheckclean, script compiles, workflow YAML parses.Models: readonly — no repo/contents/org scopes. Least privilege: it does inference and nothing else. Ideally on a machine account, not a person, so it survives staff changes.MODELS_PAT, visibility all repositories (the gate runs in each target repo, so a secret on.githubalone won't reach it).Caveats worth knowing: inference is attributed to that account's allowance and is rate-limited (fine for major bumps, which are rare), and the token needs rotating like any credential. The cleaner long-term fix remains org Models entitlement/billing, which needs no credential at all — this unblocks us meanwhile.
🤖 Generated with Claude Code