fix(settings,pm-dispatch): deny mcp__github__update_pull_request and route the ready flip through the CCR routes - #18276
Conversation
…route the ready flip through the CCR routes `permissions.deny` listed 14 `mcp__github__*` write tools and omitted `mcp__github__update_pull_request`, the MCP tool that edits a PR's title and body and flips its draft bit — a content write the charter declares closed (SKILL.md: 内容写只走 REST 代理, ⛔ 无 MCP 写). Add the entry so the enforced side matches the declared side. The same tool was the seat's fallback for the review request and the ready flip. Rewrite those two lines in place (no line added or removed, both under the 120-byte cap): review requests go through the REST endpoint that leaves the draft bit alone, ready/draft flips go through the CCR routes rest-channel already carries, and there is no MCP fallback. `enable_pr_auto_merge` is NOT denied (it is hook-guarded by guard-governed-enqueue.sh), so rest-channel says only that auto-merge keeps an undenied MCP alternative. Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)— skills 席,session
|
|
Ruling-C landing provenance — skills seat, session Generated by Claude Code |
|
Maintainer reply on record (director seat, 2026-09-15T15:05Z): batch #135 presented this PR as a governed draft row — 「改了什么:deny 表加 Generated by Claude Code |
Fixes #18218
What
.claude/settings.jsonpermissions.deny(lock 1) listed 14mcp__github__*write tools and omittedmcp__github__update_pull_request— the MCP tool that edits a PR's title and body and flips its draft bit, a content write the charter declares closed (SKILL.md 「内容写只走 REST 代理,⛔ 无 MCP 写」) and names in the ACCEPT refusal. The enforced side was open while the declared side was closed. The same tool was also the seat's documented FALLBACK for the review request and the ready flip (SKILL.md 「MCP 兜底显式带draft: true」; rest-channel 「备用通道 MCPupdate_pull_request与enable_pr_auto_merge」), so denying it and rerouting the fallback are one stroke.Three files, three hunks, nothing else:
.claude/settings.jsonmcp__github__create_pull_request(content writes lead the list; 14 → 15 entries; JSON valid; file otherwise byte-identical).claude/skills/pm-dispatch/SKILL.md:622.claude/skills/pm-dispatch/references/rest-channel.md:53SKILL.md :622 now reads 「请审走免碰 draft 位的 REST 专用路,ready/draft 走 ccr 路;MCP 兜底已拒;端点见 platform-readings。」 — review requests go through the dedicated REST endpoint that leaves the draft bit alone (
POST .../pulls/{n}/requested_reviewers), ready/draft flips go through the CCR routes (POST .../pulls/{n}/ccr/ready_for_reviewand.../ccr/convert_to_draft), there is no MCP fallback, and endpoint facts stay in platform-readings.rest-channel :53 now reads 「⛔ 永不 MCP
update_pull_request(锁 1 已拒);ready/draft 翻转只走 ccr 路;auto-merge 备用 MCP 未拒。」 — the same house form as :39'sissue_writeline. The auto-merge clause is deliberately narrow:mcp__github__enable_pr_auto_mergeis NOT in the deny list; it is guarded by the PreToolUse hookguard-governed-enqueue.sh(matcher at settings.json :82), a different mechanism from a deny. The line states only what the deny list enforces.The frame block SKILL.md :734–:755 is byte-identical (md5
3327d02c56f8a0eca88569dad2270f32).Why this shape (four axes; the one design choice: drop the MCP fallback vs keep a fallback the deny list forbids)
Applied: deny + reroute. Keeping the fallback would keep a declaration the runtime does not honour.
Gates (run on the tree of
79ee821e, this PR's only commit; exit codes captured before any pipe)Derived by
node scripts/pm/dispatch-gates.mjs --commandson the three paths — 17 commands, every one run:node scripts/check-closing-keyword-parity.mjsnode scripts/check-closing-keyword-parity.mjs --self-testnode scripts/check-comment-mask-corpus.mjsnode scripts/pm/check-governed-queue-guard.mjs --self-testpnpm --filter @objectstack/lint run check:doc-formula-expressions@objectstack/formula+@objectstack/lint); built underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0, 4 tasks cached) and re-run: 0 (58 self-test cases; 22 examples across 438 files clean)pnpm check:agent-test-spellingpnpm check:doc-authoringpnpm check:driver-memory-censuspnpm check:nul-bytespnpm check:pm-governed-merges(its script is--self-test)pnpm check:pm-governed-prosepnpm check:pm-half-statespnpm check:pm-skill-id-lintpnpm check:pm-skill-ratchetpnpm check:refd-timer-probepnpm check:skill-frame-syncpnpm check:watch-hint-literalnode scripts/pm/dispatch-gates.mjs --ran ran.list→ exit 0: 「17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED」.Beyond the derivation — everything that parses
.claude/settings.json, plus what the card named:.claude/hooks/guard-governed-enqueue.selftest.shenable_pr_auto_merge+merge_pull_request).claude/hooks/guard-main-checkout-bash.selftest.sh.claude/hooks/guard-main-checkout.selftest.sh.claude/hooks/guard-process-kill.selftest.sh.claude/hooks/guard-shared-stash.selftest.sh.claude/hooks/guard-tree-enum.selftest.shpnpm check:pm-dispatch-gates--format json --no-error-on-unmatched-pattern)eslint.config.mjspopulates only**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; JSON and Markdown are outside its population, so ESLint/format is not applicable here — measured, not assumednode scripts/pm/label-write.mjs --self-testNot run, and why:
pnpm lintrepo-wide is CI's run, and the population probe above shows the diff is outside ESLint's population, so the narrowing excludes nothing; packagebuild/test/typecheck— the diff touches no package (packages/**untouched, noexports, no publishedfiles[]content).scripts/pm/check-harness-current.mjsis a seat-side fire-time reading, not a CI gate, and answers a question about the shared checkout rather than this diff.Reverse verification: not staged as an ablation — no gate reads the deny roster's membership (census:
permissions.denyoccurs underscripts/only inside comments ofcheck-skill-line-ratchet.mjsandlabel-write.mjs), so reverting the deny entry would move zero diagnostics by construction. That absence is recorded under Acceptance notes rather than presented as a green ablation.Changeset
skip-changeset: the diff is entirely under.claude/**(the fast lane — nothing under any released package'sfiles[]); no.changeset/*.md.维护者速读(草稿)
改了什么:把
mcp__github__update_pull_request(MCP 里改 PR 标题/正文、翻 draft 位的工具)加进.claude/settings.json的 deny 清单(14 → 15 项);同时把 pm-dispatch 章程里两处「MCP 兜底」改写为只走 REST/CCR 路(SKILL.md :622、rest-channel :53),各改一行、行数不变。为什么改:章程已声明「内容写只走 REST 代理,⛔ 无 MCP 写」,ACCEPT 也拒收点名该工具的报告,但 deny 清单没拦它——声明与强制不一致。加 deny 之后原来的 MCP 兜底就是运行时必拒的路,所以兜底文字同笔改掉,不留一条走不通的路。
风险与代价(含回滚):deny 只对新开会话生效(harness 在会话启动时读 settings,不热加载),在飞会话不受影响;auto-merge 的 MCP 工具(
enable_pr_auto_merge)本单不拒,仍由 PreToolUse 钩子按治理面守着。回滚 = revert 本 PR 的唯一提交(三文件三处)。席位意见:
你要做的:在本 PR 上给一个授权批准(规则层受管面);其余由 skills 席位落地。
Acceptance notes
references/platform-readings.md:43, :46, :47 and :338 still describe MCPupdate_pull_requestas a live channel (「MCP-only 席才用 update 加显式draft: true」, 「MCP 传draft: false」). They are measured facts about the tool rather than routes in this PR's surface, and the platform-readings fold is in flight on that file, so the seat decides where their retirement lands. [finding] platform-readings: a phrase that only exists after joining (string concatenation, markdown emphasis, a wrapped line) reads 0 under a single-line grep in both directions — the instrument row PR #18194 could not fit into the rules layer #18195 is not addressed here.settings.jsondeny 清单 +update_pull_request」 — true before and after (the tool is now inside the deny list), one line outside this card's surface. 承接者:none.check:pm-skill-line-ratchetand the governed-merges self-test--test; thepackage.jsonscripts arecheck:pm-skill-ratchetandcheck:pm-governed-merges(which runs--self-test). Both were run aspackage.jsonspells them.Generated by Claude Code
Generated by Claude Code