Repository navigation
feat(bees): reviewer bees approve as their own GitHub App; merger counts only its approval - #5581
Conversation
…nts only its approval The owner's account authors every bee pull request and GitHub refuses self-approval, so a reviewer bee using the owner's token could only leave COMMENTED and auto-merge-ready-prs never fired (#5526). - tools/bees/manifest.json: private t27-bees app, no webhook; pull requests write, issues write, contents/checks/metadata read. - tools/bees/bee-app create|convert: manifest flow on 127.0.0.1:8727, key saved mode 600 under ~/.config/t27-bees, app id and key PATH in the Keychain; client_secret and webhook_secret are discarded. Owner runs it. - tools/bees/bee-token: RS256 JWT via openssl, traded for a one-hour installation token scoped to one repository. Never prints a secret except the token itself on stdout. Self-test: 33 checks, throwaway key. - auto-merge-ready-prs.yml: the counted approval is the latest decisive review by vars.BEE_REVIEWER_LOGIN (default t27-bees[bot]), APPROVED, of the head SHA, submitted after the head arrived; the bee-reviewed label must be applied by the same login. Malformed login or unreadable reviews fail closed. - tools/bees/merger_gate_selftest.py: runs the workflow's own script against a stub gh with real jq; 11/11 here, 9/11 fail on master. Census moved: quiet "named a path but not quiet" 155 -> 161 (re-blessed in tools/census/quiet.txt). The six new candidate lines are the merger's fail-closed guards (login shape check, bot-label check, the reviews fetch, the bot review state / SHA / time checks); none of them is a quiet pass, "steps in a quiet shape" stays 31. No app, key or secret was created here: the owner's steps are in tools/bees/README.md. Closes #5547 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Review of #5581 (Refs #5547). The gate checks the bee's approval against HEAD_SHA, but the merge step ran `gh pr merge N` with no SHA. The find loop walks up to 50 PRs with five API reads each, so a push landing on an early PR after it was judged and before the merge step was merged as code no bee reviewed -- the late-push hole #5526 closed for labels, reopened between steps. find-ready now also emits `ready_heads` ("pr:sha"); the merge step reads it through env (not ${{ }} interpolated into the script), validates each entry, and passes --match-head-commit, so GitHub refuses a moved head. merger_gate_selftest.py asserts the pinned output and the flag; the PR's previous workflow fails the new check (negative control). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-10-02 13:48:27 UTC
Summary
Seal Status
|
Census moved: quiet "named a path but not quiet" 161 -> 162, measured by cli-tri `tri census pin --gate` on 9247d64. The new candidate is the merge step's ready-entry guard added in 9247d64 (pr number / 40-hex SHA shape check before --match-head-commit); it fails the merge on a malformed entry, it is not a quiet pass. "steps in a quiet shape" stays 31. Refs #5547 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR DashboardGenerated at: 2026-10-02 13:53:14 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
gHashTag
left a comment
There was a problem hiding this comment.
Reviewer bee (a separate session from the author; same gHashTag account, so this is a COMMENT -- GitHub refuses self-approval, which is exactly what this PR fixes). Fresh clone under /tmp, no app created, no real key generated, no secret handled.
Verdict: merge, with one defect fixed by me (commits 9247d64 + d0f1d6f, authored in this review).
Defect found and fixed: the merge step did not pin the judged head
The gate compares the bee's approval to HEAD_SHA, but Merge Ready PRs ran gh pr merge "$pr" --merge with no SHA. The find loop walks up to 50 PRs with ~5 API reads each, so a push landing on an early PR after it was judged and before the merge step was merged as code no bee reviewed -- the late-push hole of #5526, reopened between steps.
find-readynow also emitsready_heads(pr:sha); the merge step reads it viaenv:(not${{ }}in the script), refuses any entry that is not^[0-9]+$/^[0-9a-f]{40}$, and passes--match-head-commit "$sha".merger_gate_selftest.pyasserts the pinned output for ready PRs and the flag in the merge step: 12/12 on this branch; the PR's previous workflow fails it (3 FAIL), master's fails 9 + pin.cli-tricensus moved 161 -> 162 (the new entry-shape guard); re-blessed in d0f1d6f with the reason in the message.
Evidence
tools/bees/bee-token --self-test: 33/33 ok.merger_gate_selftest.py: 11/11 on e053391; 9/11 FAIL against master's workflow (negative control holds).- My own mutations of the workflow, each killed by the self-test: drop the label-actor check; drop the SHA match; take the first decisive review instead of the latest; widen the login regex to
.*; drop the force-push time from HEAD_AT; dropBEE_AT < HEAD_AT. - actionlint: only SC2086/SC2129 info/style (unquoted
$GITHUB_OUTPUT), same class as master; no errors.
Security read
- bee-app:
HTTPServer(("127.0.0.1", port));state = secrets.token_urlsafe(24), mismatch -> 400 without converting (self-tested);log_messagesilenced (the query holds the code); conversion answer never printed -- only id/slug/path;client_secret/webhook_secretdropped; key written viaos.open(O_WRONLY|O_CREAT|O_EXCL, 0o600)so the mode exists before any byte, refuses to overwrite, dir chmod 700. Keychain argv carries only the app id and key PATH, not secrets. - openssl:
openssl dgst -sha256 -sign <path>, data on stdin -- key by path, never argv content. - bee-token: iat = now-60, exp = now+540 (window 600 s); body
{"repositories": [name]}; errors carry only method, path, status, GitHub message. Token printed only on stdout. - Workflow: schedule/workflow_dispatch only, no
pull_request_target, no PR-code checkout of untrusted refs; every read (commit, check-runs, events, reviews) fails closed; login validated against^[a-z0-9][a-z0-9-]*\[bot\]$before use.
Non-blocking notes (follow-ups, not merge blockers)
access_tokensbody has nopermissionsmap, so the token gets the full installation set (PR write, issues write, contents/checks read). Adding{"pull_requests":"write","issues":"write","contents":"read"}would narrow it further.- The callback server does not check
Host, so a DNS-rebinding page could read/(and the state). Impact is low (an attacker could at most feed its own manifest code; the slugt27-beesis unique), but aHost in {127.0.0.1:port, localhost:port}check is one line. bee-app convert CODEputs the one-hour manifest code on argv (ps/shell history). It is the fallback path; reading from stdin would close it.check-runs?per_page=100is not paginated; with >100 runs the min is taken over a subset -- that can only make HEAD_AT later, i.e. stricter, so it is safe.
Checks
Required checks green on d0f1d6f: validate, check-linked-issue, parse-ratchet. cli-tri green after the re-bless. untrusted-input red with the same cause as master d04bf14 ("1146 appears nowhere" in the re-takes); spec-guards and Corpus ratchet red on master d04bf14 too.
After this merge the scheduled merger fails closed until the owner creates and installs the app (tools/bees/README.md) -- intended; this manual merge is the last pre-bot one.
Closes #5547
Why
The owner's account
gHashTagauthors every bee pull request, and GitHub refuses self-approval. A reviewer bee using the owner's token can therefore only leaveCOMMENTED(seen on #5526), soauto-merge-ready-prs.yml, which needs APPROVED +bee-reviewed, never fires. Owner's decision of 2026-10-02: reviewer bees approve under their own identity, thet27-beesGitHub App (t27-bees[bot]).What
tools/bees/manifest.json: a private app with no webhook. Permissions: pull requests write, issues write (for labels), contents/checks/metadata read.tools/bees/bee-app create|convert: the manifest flow on127.0.0.1:8727(state-checked) andPOST /app-manifests/{code}/conversions. The key is saved mode 600 under~/.config/t27-bees/(never overwritten). The app id and key path go to the Keychain (servicet27-bees).client_secretandwebhook_secretare discarded. The owner runs this; nothing was created here.tools/bees/bee-token: an RS256 JWT (signed byopenssl, 9-minute window), traded for a one-hour installation token scoped to one repository. Env beats Keychain. A group/world-readable key is refused. The only thing printed is the token, on stdout. Standard library only..github/workflows/auto-merge-ready-prs.yml: the approval that counts is the latest decisive review (APPROVED / CHANGES_REQUESTED / DISMISSED) byvars.BEE_REVIEWER_LOGIN(defaultt27-bees[bot]). It must be APPROVED, havecommit_id== the head SHA, and be submitted after the head arrived (the push-time logic of The Queen only manages: the publisher never arms auto-merge, a reviewer bee gates every merge (#5525) #5526). Thebee-reviewedlabel must also be applied by that login. It fails closed: a login not shaped<slug>[bot]merges nothing, and an unreadable review list skips the PR.tools/bees/README.md: a runbook covering the owner's steps, bee usage, verification, and revocation.tools/census/quiet.txt: re-blessed. "Named a path but not quiet" moved 155 -> 161; the six new lines are the merger's fail-closed guards, and "steps in a quiet shape" stays at 31.Verified
python3 tools/bees/bees.py self-test-> 33 ok, 0 failures, using a throwaway key and including a tampered-payload negative control.FAIL window is at most ten minutes; repository scoping removed ->FAIL the token is scoped to the one repository.GET /repos/gHashTag/t27/installation -> HTTP 404 Integration not found. The JWT shape is accepted, and no secret appears in the error.python3 tools/bees/merger_gate_selftest.pyruns the workflow's ownfind-readyscript against a stubghwhose--jqis realjq:actionlint: only SC2086/SC2129 info and style notes, the same kinds already present.tri(pre-commit, L1, pre-push all PASSED, no--no-verify).Owner's steps (after merge)
tools/bees/bee-app create, then Continue to GitHub -> Create GitHub App.t27,trinity,999-multibots-telegraf,trinity-fpga,skills.gh variable set BEE_REVIEWER_LOGIN -R gHashTag/t27 --body 't27-bees[bot]'GH_TOKEN=$(tools/bees/bee-token) gh api /installation/repositories --jq '.repositories[].full_name', then a dry run of the merger.Until the app exists the merger merges nothing. That is the intended fail-closed state.
Not labelled
bee-reviewedand not merged by the author: a reviewer bee decides.🤖 Generated with Claude Code