Repository navigation
fix(set-main): scope replace to the caller’s own Main Bot (#613) - #617
Merged
Merged
Conversation
set-main checked only body.id, so a member could hand another owner's Main Bot's place to their own Bot: the swap both promoted theirs to the deployment-wide Main list and demoted the other owner's Main Bot. The endpoint now checks the Bot named in replace, and setMain refuses a replace across owners so the state invariant holds for every caller. Tests: core/test/authz.test.js and core/test/roster.test.js, one per half; docs/subsystems/core-host.md and a changelog fragment go with it.
Collaborator
Author
|
Verdict: PASS at d974ee6 Ran at this tip, not inferred from CI: Gate ③, both directions — Exactly the two new tests go red, and only they. What I checked beyond the tests:
One note, not a blocker: the branch is based on Entering the merge queue now. |
Collaborator
Author
|
Post-merge record: |
This was referenced Oct 21, 2026
Birfy
added a commit
to Birfy/FlyBot
that referenced
this pull request
Oct 23, 2026
…#670) Fixes ThinkFlowLab#609 Tip SHA: 4c8d98c ## Reproduce The rule text says `risk:T*` is set by the controller from the diff, never by hand, and the mechanism does not hold up: `github-actions[bot]` labelled ThinkFlowLab#574 at 07:28:43Z, while ThinkFlowLab#606, ThinkFlowLab#610, ThinkFlowLab#611, ThinkFlowLab#612, ThinkFlowLab#616, ThinkFlowLab#617, ThinkFlowLab#618 and ThinkFlowLab#619 carry no `risk:T*` at all even though `risk tier (report only)` succeeded on every one of them. The owner decision on the issue is ②: keep the workflow read-only, never label — and make the computed tier auditable instead of remembered. At this tip, `node .github/scripts/risk-gate.mjs --pr <n>` ends its report with risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3 so a review can quote the tier the rules produced instead of restating a number. ## What changed - `.github/scripts/risk-gate.mjs`: `judge()` records every changed path (and a rename's old path) with the tier `tierOf()` gave it, and `summaryLine()` renders the final line of the report from that same result — highest tier first, paths capped at 10 with a `+N more` tail. `render()` ends the job summary with it, after the prose, so the two can never disagree. - `.github/scripts/risk-gate.test.mjs`: four cases — the line's exact shape for a T3 PR, the ordering and the cap, the no-changed-files case, and "the line is the last line of the report". - `AGENTS.md` gains a bullet under the label section: quote the summary line, the workflow never writes a label, the controller copies it on its poll, no review transcribes a tier. - `.github/labels.json`: every `risk:T*` description keeps `advisory` and `the merge gate recomputes it` and now also names the controller as the writer (all ≤ 100 characters). - `docs/controller.md`: the tier stays auditable without the label — quote the summary line. - `.github/workflows/risk-tier.yml`: header comment only — no permission added, no step changed. It keeps `contents: read` and `pull-requests: read` and still never labels, comments or merges. ## What I ran - `TZ=UTC node --test .github/scripts/*.test.mjs` at this tip (with `git` on `PATH`) → **144/144**. - `node --test .github/scripts/risk-gate.test.mjs` → **18/18**, including the four new cases. - `node .github/scripts/risk-gate.mjs` behaviour checked by hand through the harness result: `judge([AGENTS.md, plugins/social/src/social.js])` → `risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3`; no files → `risk-tier: T0; basis: none (no changed files)`. - `changelog.mjs check` not run: a change users cannot see needs no fragment (changelog.d/README). ## Red without the source change - Removing the `basis.push([name, t])` line from `judge()` (the tests untouched) → `risk-gate.test.mjs` **16/18, 2 red**, exactly `summaryLine: one machine-readable line…` and `summaryLine: highest tier first…`. - Removing `summaryLine(result)` from the end of `render()` instead → **17/18, 1 red**, exactly `render: the machine-readable line is the last line of the report`. Restored → 18/18. ## Not run - The workflow itself (it needs Actions): the change to `risk-tier.yml` is a comment, and the existing `the workflow stays report only` test still passes with it. - `npm run verify` end to end: no `core/node_modules` in this sandbox (the same on `main`). - The `risk-tier` job on this PR runs the base copy of the script, so its summary will only show the new line after merge — that is the workflow's normal base-copy behaviour, not a gap here. ## Risk T3 by the repository's own rules (`.github/**`, `AGENTS.md`): a reporting script plus text. It adds no permission, no network call, no label, comment or merge, and cannot block anything — the job is still `continue-on-error: true` and not a required check. The only behaviour it changes is the last line of that job's summary. The one thing a reader must not conclude is that the label now arrives on time: it does not, and the docs keep saying why (ThinkFlowLab#629).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
set-mainchecked onlybody.id, so a signed-in member could take another owner's Main Bot's place:{ id: <my Bot>, replace: <their Main Bot> }promoted their own Bot into the deployment-widestate.mainBotIdsand demoted the other owner's Main Bot, whose Bot then lost the Main-only tools. This is option 1 of #613: owner-scopedreplace, the smallest change that closes the demote path exactly.What changed
core/src/host/authz.js—set-mainkeeps thebody.idcheck and now also requires the actor to ownbody.replacewhen it names a different Bot. Taking another owner's Main Bot's place isbot/forbiddenbefore the roster runs.core/src/host/roster.js—setMainrefuses areplacewhose Bot has a differentownerIdand leavesstate.mainBotIdsunchanged, so the invariant holds for every caller of the roster, not only the HTTP endpoint.core/test/authz.test.js,core/test/roster.test.js— one test per half.docs/subsystems/core-host.md(thecore/src/host/owner),changelog.d/613-main-bot-owner.security.md, and the regeneratedpatches/ds-bot/(node scripts/ds-bot-patches.mjs generate+check).Deliberately not in this PR: a per-owner
state.mainBotIds(option 2) andblocked-sharedforset-mainwith more than one member (option 3). Both are the team-mode decision #613 leaves open; today's shape — one deployment-wide list, where a member may still promote a Bot they own — is unchanged.撤掉修复后变红
Reverted the two source hunks only (tests kept,
authz.jsback toif (ep === 'set-main') return requireBot(body?.id)andsetMainback to its oldreplacebranch):Exactly the two new tests fail; the other 46 in those files pass.
修复后变绿
Same command with the fix in place:
The authz test covers: a member's swap of another owner's Main Bot is
bot/forbidden; demoting that Main Bot by naming it as the target is refused; a swap inside one owner's team is allowed; the caller's own Bot may still be promoted plainly. The roster test covers the cross-ownerreplacebeing refused withstate.mainBotIdsunchanged on disk, and a same-owner swap still working.跑了什么
node .github/scripts/changelog.mjs check→ 23 fragments well formed.node core/scripts/check-arch-docs.mjs→ OK (6 changed file(s)) against a localorigin/mainat81c708d.node scripts/ds-bot-patches.mjs generatethencheck→core/ equals ds-bot/ + patches/ds-bot/.没跑什么
npm run verify: this box has nonpm ci/npm run harnessinstall forcore/(oxlint comes with it), so lint and the whole core suite run on CI here. CI'stests-okcovers lint,arch-docs, the client check, the patch check and the full core + plugin suites.packagejob: not run locally; unchanged code paths, and CI runs them.set-mainin a live deployment; the two-owner behaviour is asserted at the authz and roster layers.风险
Permissions surface. The change only narrows: it adds an owner check on
replaceand an owner invariant insetMain. No new configuration, no secret path, no change tort.isMainor to the tool gates it drives.risk:T*is set by the controller.Fixes #613