Skip to content

fix(settings,pm): deny the MCP enqueue class — enable/disable_pr_auto_merge join lock 1 and the roster gate - #18317

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-18282-deny-enqueue-class
Draft

claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-18282-deny-enqueue-class

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #18282

Draft, rules layer — stays a draft until an authorized APPROVED review. The diff touches .claude/settings.json (a governed surface under Prime Directive 14), so no seat readies, queues, or arms auto-merge on it. Landing follows ruling C on card #17971 (maintainer direct ruling, 2026-09-13, comment 5652306063: 「C. approve 后不管后续改动都由席位落地:」) — after the authorized approval the owning seat lands it; no reviewer request comes from the dev.

The ruling this implements — director seat, comment 5682626575 on #18282, verbatim and untranslated

A — mcp__github__enable_pr_auto_merge and mcp__github__disable_pr_auto_merge join permissions.deny in .claude/settings.json (lock 1, 15 → 17 entries); guard-governed-enqueue.sh stays as defence in depth; references/rest-channel.md :53 drops the 「auto-merge 备用 MCP 未拒」 clause. The REST route (PUT …/pulls/N/ccr/auto_merge, rest-channel :50) already serves every real enqueue, so nothing a seat does today is lost.

Rules layer: the change lands as a four-piece governed draft on an authorized APPROVED, ⛔ never armed or merged by a seat; the objectui mirror of lock 1 (PR objectui#9448) gets the same two entries in its own card once this lands.

Class-1 self-adjudication, batch #135 追认, the maintainer's 「135 同意」.

What changes — three files, nothing else

  1. .claude/settings.jsonmcp__github__enable_pr_auto_merge and mcp__github__disable_pr_auto_merge appended to permissions.deny after mcp__github__fork_repository, in the file's own write-class order (issue → PR → comments → review → files → branch → sub-issue → merge → repo → enqueue). 15 → 17 mcp__github__ entries; every other byte of the file unchanged. The guard-governed-enqueue.sh matcher on line 82 still names enable_pr_auto_merge — that is the hook's registration, not a permission, and it stays.
  2. scripts/pm/check-settings-deny-roster.mjs (the gate PR feat(pm): a gate pins permissions.deny to the charter's content-write roster #18285 landed) — CONTENT_WRITE_TOOLS gains the same two names in the same order (15 → 17; still the ONE declaration). The header paragraph that said the enqueue class is NOT in the roster is re-keyed to say it IS, ruled closed, and why; the membership rule above the constant now covers "arms or disarms a pull request's landing" so the criterion and the constant agree. A new self-test battery the enqueue class — declared closed (4 fixture cases: both names in the roster; a document denying exactly the roster reads declared = enforced; a document missing one of the pair names it); SELF_TEST_BATTERY_FLOOR 7 → 8; self-test 32 → 36 cases.
  3. .claude/skills/pm-dispatch/references/rest-channel.md line 53 — the 「auto-merge 备用 MCP 未拒」 clause is dropped in place; the row now ends 「auto-merge MCP 锁 1 同拒。」. 118 → 117 bytes, one line stays one line, the file stays 82/82 on the ratchet, no ruledRaises owed.

Untouched by design: .claude/hooks/guard-governed-enqueue.sh and its self-test (defence in depth, per the ruling), every other row of rest-channel.md, platform-readings.md, objectui.

Why the header paragraph is re-keyed rather than deleted — the one design choice, on the four axes

Verification (all readings on this branch; final union on ad7a3da5)

Reverse verification — the count is the control.

  • Base 6ef33128 (origin/main at cut): pnpm check:pm-settings-deny-roster✓ … 15 content-write tool(s) declared = enforced in .claude/settings.json (15 mcp__github__ deny entr(ies) …); --self-test32 cases pass.
  • Base roster vs a 17-entry fixture (the two new deny entries added to main's file): exit 1 — denies mcp__github__enable_pr_auto_merge, mcp__github__disable_pr_auto_merge, which this gate's roster does not declare — roster drift. This is the direction the change removes.
  • Head: ✓ check-settings-deny-roster: 17 content-write tool(s) declared = enforced in .claude/settings.json (17 mcp__github__ deny entr(ies), 0 outside this gate's population and ignored).; --self-test36 cases pass.
  • Head roster vs a fixture missing disable_pr_auto_merge: exit 1 — does not deny mcp__github__disable_pr_auto_merge — the charter declares it closed while the enforced list leaves it open.

Ablation from the committed state, hash proofs per leg (git restore --source=BASE -- PATH for the mutation, git checkout HEAD -- PATH for the restore, a trap on EXIT/INT/TERM with absolute paths, every blob hash compared and non-empty):

  • Leg A — .claude/settings.json at the base blob 0acd039e (15 entries), gate at HEAD: exit 1, does not deny mcp__github__enable_pr_auto_merge, mcp__github__disable_pr_auto_merge … leaves them open. Restored to HEAD blob 428a0300.
  • Leg B — gate at the base blob 391f20ce (15-name roster), settings at HEAD: exit 1, denies mcp__github__enable_pr_auto_merge, mcp__github__disable_pr_auto_merge, which this gate's roster does not declare — roster drift. Restored to HEAD blob 5736aad3.
  • Restored tree: 17 = 17 green; git diff HEAD empty; git status --porcelain empty.

The hook is untouched and its self-test reads the same before and after. git diff --stat 6ef33128 ad7a3da5 -- .claude/hooks/ is empty. bash .claude/hooks/guard-governed-enqueue.selftest.sh reads 54 passed, 1 failed on the untouched base and byte-identically on the head (diff of the two logs empty). The one red is pre-existing and environmental, not this PR's: the case an exception-row path in a repo this container cannot resolve expects allow on the premise that no sibling cloud checkout exists, and this container has /home/user/cloud, which the hook resolves at its lines 453–454 and recomputes against, so it blocks. See Acceptance notes.

Derived gate union on the merge commit ad7a3da5 (node scripts/pm/dispatch-gates.mjs --commands, 40 commands, identical list before and after merging origin/main; each exit captured by redirect, then --ran):

  • ✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 39 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
  • 38 commands exit 0, including pnpm check:pm-settings-deny-roster, pnpm check:pm-skill-ratchet (rest-channel.md is 82 lines (ceiling 82; headroom 0)), pnpm check:pm-skill-id-lint, pnpm check:pm-half-states (on the feat(pm): the half-state patrol reads the tier a triage round artefact names #18311 copy, 4196 cases pass), pnpm check:nul-bytes, pnpm check:watch-hint-literal, pnpm check:skill-frame-sync, pnpm check:pm-governed-merges, node scripts/check-self-test-wired.mjs (+ --self-test).
  • pnpm check:pm-dispatch-gates (the battery) run detached: exit 0, dispatch-gates self-test: 1730 cases pass, 454 s on this box (462 s on the pre-merge run).
  • NOT MEASURED: pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3, PREREQUISITE NOT MET — the workspace package @objectstack/formula is not built. The diff touches no package; building @objectstack/formula means building @objectstack/spec first, so this gate is left to CI, where it runs on a built tree. Declared narrowing, not a skip.
  • ESLint on the gate file: exit 0 before and after. Control-character self-scan over the three files: no hits.

skip-changeset — every path is in the never-published lane (.claude/**, scripts/pm/**); no package files[] moves.

Acceptance notes

  • Out of scope, for the seat to card (as [finding] platform-readings: three rows of the PR-body footer block still read the denied MCP update_pull_request wrapper as a live edit channel (:347 · :348 · :354 on the PR #18307 head) #18310 did for the update_pull_request rows): references/platform-readings.md still describes enable_pr_auto_merge / disable_pr_auto_merge BEHAVIOUR as if the MCP tools were callable — after PR docs(pm): platform-readings retires the rows that still describe MCP update_pull_request as a live channel #18307's renumbering these are lines 41 (转 draft 与 disable_pr_auto_merge 都做), 49 (enable_pr_auto_merge 恒显式传 mergeMethod), 58 (照样成功 on mergeable_state: clean), 60 (配额枯竭时回成功). Once lock 1 denies the pair those rows describe a channel no seat can reach; the REST ccr/auto_merge readings on rest-channel 50–52 are the live ones. Not touched here — this card is three files.
  • Finding, for the seat (.claude/hooks/** is outside this card): guard-governed-enqueue.selftest.sh is not hermetic on a box with a sibling cloud checkout — the case above reads red there and green in CI. Repro: with /home/user/cloud present, bash .claude/hooks/guard-governed-enqueue.selftest.sh54 passed, 1 failed, FAIL want=allow got=block an exception-row path in a repo this container cannot resolve. Dedupe words: guard-governed-enqueue selftest sibling cloud checkout hermetic.
  • The objectui mirror of lock 1 (PR objectui#9448) gets the same two entries in its own card once this lands — per the ruling, not this PR.

维护者速读(草稿)

改了什么 — 把 MCP 的 enable_pr_auto_merge / disable_pr_auto_merge 两个工具加进 .claude/settings.jsonpermissions.deny(锁 1,15 → 17 条);门禁 check:pm-settings-deny-roster 的名单同步加这两条、它自己的说明改成「入队类已裁定关闭」、自测 32 → 36 例;rest-channel.md 第 53 行删掉「auto-merge 备用 MCP 未拒」,改为「auto-merge MCP 锁 1 同拒」。钩子 guard-governed-enqueue.sh 一字未动,继续作纵深防御。

为什么改 — 董事席裁决 A(#135 批追认,你的「135 同意」):真实入队全部走 REST 的 PUT …/pulls/N/ccr/auto_merge,MCP 那条备用路没人用却留着口子,靠钩子挡只覆盖受管 PR;运行时直接拒掉,席位就不可能误用,声明 = 强制的原则延伸到入队类。

风险与代价(含回滚) — 席位现有流程零损失(所有真实入队都走 REST)。风险只有一种:某个会话若真去调这两个 MCP 工具会被运行时拒绝——这正是目的。回滚 = revert 本 PR 的一个 commit;门禁把 deny 与名单钉成相等,任何半回滚都会立刻变红,不可能静默存在。

席位意见 — (留空,席位定稿)

你要做的 — 审阅并 APPROVE(.claude/settings.json 属 rules 层,需要授权账号的 APPROVED);之后由席位落地,不需要你合并。objectui 镜像(PR objectui#9448 的锁 1)另开一张卡跟进,不在本 PR。


Generated by Claude Code

…_merge join lock 1 and the roster gate

`permissions.deny` in `.claude/settings.json` is the enforced half of the
dispatch charter's rule that GitHub writes go through the REST proxy and
never through an MCP write tool. The enqueue pair —
`mcp__github__enable_pr_auto_merge` and `mcp__github__disable_pr_auto_merge`
— was the one MCP write class left outside it: hook-guarded only, so a
non-governed enqueue could still reach GitHub through MCP under the bound
user account, and the prose had to name the exception.

The director seat ruled A: the pair joins lock 1. The REST route
`PUT .../pulls/{n}/ccr/auto_merge` already serves every real enqueue, so
nothing a seat does today is lost, and a tool the runtime refuses cannot be
mis-called by a seat that mis-reads the hook's scope.

## What lands

- `.claude/settings.json` — the two names appended to `permissions.deny`
  beside the fifteen `mcp__github__` entries (15 → 17), nothing else moved.
- `scripts/pm/check-settings-deny-roster.mjs` — `CONTENT_WRITE_TOOLS` gains
  the two names in the file's order (the one declaration); the header
  paragraph that said the enqueue class is NOT in the roster now says it IS
  and why; the membership rule above the constant covers arming/disarming a
  landing; a new self-test battery of four fixture cases pins both names,
  a document denying exactly the roster reading declared = enforced, and a
  document missing one of the pair naming it; battery floor 7 → 8,
  self-test 32 → 36 cases.
- `.claude/skills/pm-dispatch/references/rest-channel.md` line 53 — the
  「auto-merge 备用 MCP 未拒」 clause is dropped in place; the row now says the
  auto-merge MCP pair is denied by lock 1 too (118 → 117 bytes, one line
  stays one line, the file stays at its 82-line ceiling).

`guard-governed-enqueue.sh` and its self-test are untouched: the hook stays
as defence in depth. No changeset — nothing published moves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 15, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ad7a3da5a8b19597382d78689fe1ffb48d43a5a3

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T07:15Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18282 (a decision card RULED A by the director seat, 5682626575, class-1 self-adjudication under the maintainer's 「135 同意」), claim 5683705838, report 5684269831 (posted through the tool after two offline stamp-contract refusals, nothing duplicated); Clause-②: no on the claim; the dev held at the review tier by the seat's judgment (rules layer); seat review at tier.

① Derived judgments

  • Accept set / public surface: none moves — .claude/settings.json (+2 deny entries, 15 → 17), scripts/pm/check-settings-deny-roster.mjs (+40/−15: CONTENT_WRITE_TOOLS 15 → 17, the header's 「deliberately NOT asserted」 paragraph re-keyed to 「the ENQUEUE class is in the roster」, one new 4-case battery, floor 7 → 8, self-test 32 → 36), .claude/skills/pm-dispatch/references/rest-channel.md :53 in place (「auto-merge 备用 MCP 未拒」 → 「auto-merge MCP 锁 1 同拒」, 118 → 117 B, 82 / 82) against merge-base 10a7b9fa; guard-governed-enqueue.sh and its self-test untouched (diff-stat over .claude/hooks/ empty); nothing published; skip-changeset read back. Rules layer ⇒ four-piece: this record, the 速读终稿 below, ACCEPT on the card, needs-user-decision + reviewers; lands by ruling C after an authorized APPROVED review.
  • The change, read in full on a detached worktree of the head: exactly the ruling's letter A — the two enqueue tools denied at the harness, the gate's one declaration extended so 17 = 17 in both directions, the register row pointing at lock 1 rather than enumerating the roster a second time. Seat re-run on the head: pnpm check:pm-settings-deny-roster 「17 content-write tool(s) declared = enforced (17 mcp__github__ deny entries, 0 outside)」 with 36 self-test cases; origin/main reads 15 = 15 / 32; ESLint 0/0 on the gate; check:pm-skill-ratchet rest-channel 82 / 82; check-self-test-wired OK; --pair 18317 0; the deny read on the head names both tools.
  • Gates 40 derived / 39 run / 1 NOT MEASURED (check:doc-formula-expressions, exit 3 — @objectstack/formula unbuilt; a prose-and-gate diff touching no package, CI runs it built) by --ran on the merge head, the battery 1730 pass twice (462 s pre-merge, 454 s on ad7a3da5). Reverse verification by the dev: base 15 = 15; a 17-entry fixture against main's roster → 「roster drift」 naming both (the direction this change removes); head 17 = 17; a 16-entry fixture against the head's roster → 「does not deny mcp__github__disable_pr_auto_merge」; two ablation legs with blob hashes (settings ← base: containment finding; gate ← base: drift finding; restored, git diff HEAD and git status --porcelain empty); the hook self-test 54 / 1 on base and head with an empty diff of the logs — the one red is pre-existing and environmental (a sibling /home/user/cloud checkout, which the SEAT itself attached at 2026-09-15T13:15Z for the governed-merge audit, makes the case 「an exception-row path in a repo this container cannot resolve」 resolve), filed as its own card below.

② Semver level

Not applicable — nothing published moves; skip-changeset measured.

③ Boundary flags

open_questions: none. One seat premise refuted, owned here: 「the hook self-test still passes untouched」 — it reads 54 / 1 on this box before and after, environmental, hooks untouched. Seven deviations answered: origin/main moved twice under the branch (fast-forward before any edit, then a merge commit, never a rebase, the union re-run on ad7a3da5) — accepted; the hook self-test red reported and filed rather than fixed — accepted, hooks are outside the card and governed; doc-formula NOT MEASURED — accepted, declared; the PR opened after the union so the body cites the union's sha — accepted; the model-free trailer pair — as required; :53 worded to point at lock 1 rather than name the pair — accepted, the gate header forbids a second roster enumeration; two offline stamp-contract refusals before the report posted — the tool doing its job, nothing duplicated. Out of scope, FILED by the seat: platform-readings :41 / :49 / :58 / :60 describe MCP enqueue behaviour a seat can no longer reach (the #18310 shape, serial behind this PR and PR #18316); the hook self-test's non-hermetic case (class (a), .claude/hooks/** governed). Noted for after the landing: the objectui mirror of lock 1 (objectui PR #9448) gets the same two entries in its own card, per the ruling. Landing: ruling C after an authorized APPROVED review by os-zhuang / hotlong — record on this head, --pair 0, checks green, clear needs-user-decision, ready through the CCR route, auto-merge SQUASH; the seat never approves.

Implemented-by: claude/issue-18282-deny-enqueue-class
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #18317 / #18282

改了什么:按总监席裁决 A(你「135 同意」):mcp__github__enable_pr_auto_mergemcp__github__disable_pr_auto_merge.claude/settings.jsonpermissions.deny(锁 1,15 → 17);deny 名单门禁 check-settings-deny-roster.mjs 的唯一声明常量同步到 17,并加一组 4 例自检;rest-channel :53 把「auto-merge 备用 MCP 未拒」改为「auto-merge MCP 锁 1 同拒」。guard-governed-enqueue.sh 不动,作为纵深防御保留。席位现在只能经 REST PUT …/pulls/N/ccr/auto_merge 入队——今天所有真实入队本就走这条路,没有能力丢失。

验证:主干 15 = 15 → head 17 = 17;两向反证(缺一项 → 报「未 deny」;多一项 → 报「roster drift」)各自命中;40 条派生门禁 39 绿 1 NOT MEASURED(需先构建 formula 包,CI 会跑);电池 1730 例两次通过。

要你做的:审 PR #18317,同意即 APPROVE;之后由本席按裁定 C 落地。落地后 objectui 的锁 1 镜像(objectui PR #9448)另立一张卡加同样两项,按裁决文本。

顺带发现(已立卡,不阻塞):hook 自检有一例在本容器上红——它假定不存在 /home/user/cloud 检出,而本席今天为了审计五仓把 cloud 附加了进来;CI 上绿。属 .claude/hooks/** 治理面,另卡处理。


Generated by Claude Code

@claude
claude Bot requested review from hotlong and os-zhuang September 15, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant