Skip to content

fix(settings,pm-dispatch): deny mcp__github__update_pull_request and route the ready flip through the CCR routes - #18276

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-18218-deny-update-pull-request
Sep 15, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-18218-deny-update-pull-request

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #18218

What

.claude/settings.json permissions.deny (lock 1) 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 写」) 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 「备用通道 MCP update_pull_requestenable_pr_auto_merge」), so denying it and rerouting the fallback are one stroke.

Three files, three hunks, nothing else:

file change bytes
.claude/settings.json one deny entry added at :55, directly after mcp__github__create_pull_request (content writes lead the list; 14 → 15 entries; JSON valid; file otherwise byte-identical) +42 (5309 → 5351)
.claude/skills/pm-dispatch/SKILL.md :622 rewritten in place, no line added or removed (812 → 812) 119 → 117
.claude/skills/pm-dispatch/references/rest-channel.md :53 rewritten in place, no line added or removed (82 → 82) 70 → 117

SKILL.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_review and .../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's issue_write line. The auto-merge clause is deliberately narrow: mcp__github__enable_pr_auto_merge is NOT in the deny list; it is guarded by the PreToolUse hook guard-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)

  • 实际业务需求: the ready flip and the review request run on every landing; measured over the seats' own charter, both already have a REST route the seats use (rest-channel :42, :46). Nothing consumes the MCP fallback that the REST route does not serve.
  • 项目长远合理性: one write channel (the REST proxy) for content, no dialect. A fallback the runtime refuses is a workaround written down.
  • 防 AI 犯错: declared = enforced. A seat that reads a fallback line and calls a denied tool sees a tool-roster absence it is told to read as the deny working (platform-readings :220); removing the line removes the trap. Contract tightening (deny) over consumer tolerance (fallback prose).
  • 创业阶段不扩散需求: no staged window, no dual spelling; the fallback retires immediately with the deny.

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 --commands on the three paths — 17 commands, every one run:

command exit
node scripts/check-closing-keyword-parity.mjs 0
node scripts/check-closing-keyword-parity.mjs --self-test 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/pm/check-governed-queue-guard.mjs --self-test 0 (238 cases)
pnpm --filter @objectstack/lint run check:doc-formula-expressions 3 on the fresh worktree (PREREQUISITE NOT MET: unbuilt @objectstack/formula + @objectstack/lint); built under scripts/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-spelling 0
pnpm check:doc-authoring 0 (401 files clean; 44 published skill files clean)
pnpm check:driver-memory-census 0
pnpm check:nul-bytes 0 (8684 text files, no raw control bytes)
pnpm check:pm-governed-merges (its script is --self-test) 0 (328 assertions)
pnpm check:pm-governed-prose 0 (28 self-test cases; 2 surfaces name all 5 governed surfaces)
pnpm check:pm-half-states 0 (4113 cases)
pnpm check:pm-skill-id-lint 0 (27 files clean)
pnpm check:pm-skill-ratchet 0 (SKILL.md 812/812, rest-channel 82/82, every 120-byte line cap held)
pnpm check:refd-timer-probe 0
pnpm check:skill-frame-sync 0 (14 self-test cases; the one declared frame copy coherent; 74 markdown files scanned for undeclared copies)
pnpm check:watch-hint-literal 0

node 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:

command exit
.claude/hooks/guard-governed-enqueue.selftest.sh 0 (55 passed, 0 failed; includes 「.claude/settings.json registers the hook」 and the matcher covering enable_pr_auto_merge + merge_pull_request)
.claude/hooks/guard-main-checkout-bash.selftest.sh 0 (130 passed, 0 failed)
.claude/hooks/guard-main-checkout.selftest.sh 0 (120 passed, 0 failed)
.claude/hooks/guard-process-kill.selftest.sh 0 (69 passed, 0 failed)
.claude/hooks/guard-shared-stash.selftest.sh 0 (53 passed, 0 failed)
.claude/hooks/guard-tree-enum.selftest.sh 0 (38 passed, 0 failed)
pnpm check:pm-dispatch-gates 0 (1730 cases)
ESLint over the three files (--format json --no-error-on-unmatched-pattern) 0 — all three 「File ignored because no matching configuration was supplied」, errorCount 0. eslint.config.mjs populates 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 assumed
node scripts/pm/label-write.mjs --self-test 0 (64 cases)

Not run, and why: pnpm lint repo-wide is CI's run, and the population probe above shows the diff is outside ESLint's population, so the narrowing excludes nothing; package build/test/typecheck — the diff touches no package (packages/** untouched, no exports, no published files[] content). scripts/pm/check-harness-current.mjs is 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.deny occurs under scripts/ only inside comments of check-skill-line-ratchet.mjs and label-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's files[]); 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

  • noted, not filed: no gate pins the deny roster against the tools SKILL.md :602 names in the ACCEPT refusal — declared roster vs enforced roster is reconciled by hand, and this PR is one such reconciliation. 承接者:the skills seat on its next settings-lock card; no open PR touches it.
  • noted, not filed (surface note; not edited): references/platform-readings.md :43, :46, :47 and :338 still describe MCP update_pull_request as 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.
  • noted, not filed: SKILL.md :602 keeps the spelling 「settings.json deny 清单 + update_pull_request」 — true before and after (the tool is now inside the deny list), one line outside this card's surface. 承接者:none.
  • The dispatch named the ratchet gate check:pm-skill-line-ratchet and the governed-merges self-test --test; the package.json scripts are check:pm-skill-ratchet and check:pm-governed-merges (which runs --self-test). Both were run as package.json spells them.

Generated by Claude Code


Generated by Claude Code

…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>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 79ee821ec7ae478207782787169a47482d2634b4

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 #18218, claim 5675922378, report 5676241066; Clause-②: no on the claim; the dev built at the tier dispatch-gates.mjs --tier mandates for the SKILL.md line, no exit taken.

① Derived judgments

  • Accept set / public surface: none moves — .claude/settings.json (+1), .claude/skills/pm-dispatch/SKILL.md (+1/−1), references/rest-channel.md (+1/−1) against merge-base 681317c3; nothing published; skip-changeset on the PR (read back with documentation, size/xs). Path face check-governed-merges.mjs --test on the three paths: .claude/** ×3 — governed, rules layer (settings + SKILL.md).
  • Deny entry: mcp__github__update_pull_request at :55, directly after create_pull_request (the list is grouped, content writes lead); 14 → 15; JSON parses on the head; the file is otherwise byte-identical (seat-diffed: one added line). The seat's assumption (1) verified by the dev against the 63-name roster: update_pull_request is the only MCP title/body/draft editor; update_pull_request_branch is a branch update with a REST twin (rest-channel :44), not a content write.
  • SKILL.md :622 in place (119 → 117 B): the review request keeps the draft-bit-safe REST endpoint, the ready/draft flip names the ccr routes, the MCP fallback reads 已拒, endpoint facts stay in platform-readings — the old line's four facts minus the one the deny makes false. rest-channel :53 in place (70 → 117 B): 「⛔ 永不 MCP update_pull_request(锁 1 已拒)」 in the house form of :39, the ready/draft flip only through the ccr routes, and — precisely — 「auto-merge 备用 MCP 未拒」: enable_pr_auto_merge is not in the deny list but hook-guarded (guard-governed-enqueue.sh, settings :82), so the line declares nothing the deny list does not enforce. 812/812 and 82/82, both touched lines under 120 B, zero id-lint hits, frame block md5 3327d02c56f8a0eca88569dad2270f32 on the head — seat-measured.
  • Gates 17 / 17 / 0 / 0 by --commands + --ran (the seat re-derived 17 on the head), exits captured by redirect; doc-formula-expressions 3 → 0 after its prerequisite build under the verify lock, recorded as the single measured reading. Seat re-run on a detached worktree of 79ee821e: skill-line-ratchet (157 + live), skill-id-lint (27 files), skill-frame-sync (14 + live), governed-prose (28 + live), half-states 4113, governed-merges self-test (live generator, 9 outputs), doc-authoring (self-test + live), guard-governed-enqueue.selftest.sh 55/0, guard-main-checkout.selftest.sh 120/0 — every exit 0; the two prerequisite-gated ones exited 3 before pnpm install --offline and were recorded as NOT MEASURED until then, never as a run.
  • No census surface states the deny count or roster (the dev's grep; assumption (2) false as hoped): reverting the entry moves zero diagnostics by construction — the absence of an ablation is recorded, not dressed as one; the gap itself is [finding] settings lock 1: no gate pins permissions.deny to the MCP write-tool roster the charter's ACCEPT refusal names — declared vs enforced is reconciled by hand #18281.

② Semver level

Not applicable — nothing published moves; skip-changeset measured (.claude/** is on the fast path).

③ Boundary flags

open_questions: none. Eight declared deviations, each answered: (1) the gate spellings check:pm-skill-line-ratchet / --test were the dispatch's — the seat's slip, owned (package.json spells check:pm-skill-ratchet and check:pm-governed-merges = --self-test; --test is the path-face flag); accepted. (2) the 3-then-0 pair on doc-formula-expressions recorded as the single measured reading — accepted, the honest form. (3) the commit trailer written model-free per AGENTS.md and the pre-push hook — accepted, the file outranks the harness line. (4) the platform appended a second footer block under the sent rule-less one — accepted; the reading goes to the register through #18280. (5) the labeler race before skip-changeset, union read back — accepted. (6) the battery detached, the wait bounded by tail --pid, never the gate — accepted. (7) no ablation staged — accepted as above. (8) platform-readings :43 / :46 / :47 / :338 still describe the tool as live — outside the surface and under the fold's region claim, NOT edited, correctly; filed as #18280. Governed (rules layer) ⇒ four-piece: needs-user-decision on the PR, 速读终稿 below, reviews requested from os-zhuang and hotlong; ⛔ not flipped ready by the seat before an authorized approval. Propagation: the deny reaches only sessions started after it lands (the harness reading behind #18205 (c)); in-flight sessions keep the roster they loaded — said in the 速读. Out of scope, recorded: SKILL.md :602's spelling stays true (the tool now sits inside the list it names); the enable_pr_auto_merge enqueue-class question is the maintainer's, filed as #18282.

Implemented-by: claude/issue-18218-deny-update-pull-request
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— skills 席,session session_01HZfg2AwVX191qCizp88gQr,2026-09-15T07:18Z

改了什么:mcp__github__update_pull_request(MCP 里改 PR 标题/正文、翻 draft 位的工具)加进 .claude/settings.json 的 deny 清单(14 → 15 项,插在 create_pull_request 之后);pm-dispatch 章程两处「MCP 兜底」原位改写为只走 REST / CCR 路(SKILL.md :622、rest-channel :53),行数不变、两行均 117 B。

为什么改:章程 :98 已声明「内容写只走 REST 代理,⛔ 无 MCP 写」,:602 的 ACCEPT 也拒收点名该工具的报告,但 deny 清单没拦它——声明与强制不一致。加 deny 之后原来的 MCP 兜底就是运行时必拒的路,所以兜底文字同笔改掉,不留走不通的路。

风险与代价(含回滚):deny 只对新开会话生效(harness 启动时读 settings,不热加载),在飞会话不受影响;auto-merge 的 MCP 工具 enable_pr_auto_merge 本单不拒,仍由 PreToolUse 钩子 guard-governed-enqueue.sh 守着,rest-channel :53 如实写「auto-merge 备用 MCP 未拒」。回滚 = revert 本 PR 唯一提交(三文件三处)。

席位意见:按四轴,契约收紧(deny)优于消费端宽容(留一条运行时必拒的兜底文字);席位复核记录 PASS(上一条评论),席位在独立 worktree 重跑门禁全绿。关联:platform-readings 仍有四行把该工具写成活通道(:43 / :46 / :47 / :338),已另立 #18280 排在合并单之后;deny 名单无门禁对照 :602 名单,另立 #18281;enable_pr_auto_merge 是否同样入 deny,是你的决定,另立 #18282(席位建议 A:一并拒)。

你要做的:在本 PR 上给一个授权批准(规则层受管面);其余由 skills 席位按裁决 C 落地。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 15, 2026 14:54
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Ruling-C landing provenance — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T14:55Z. Authorized APPROVED review 5211697198 by os-zhuang at 2026-09-15T14:54Z on head 79ee821ec7ae478207782787169a47482d2634b4, not dismissed. Landing readings on that head: review of record 5676329091 (## Contract review, PASS, names this head; 速读终稿 5676329589; ACCEPT 5676330010 on #18218); check-clause2-carriers.mjs --pair 18276 exit 0 at 2026-09-15T14:54Z (C6-RECORD found, Served-tier: at tier, no widening tell); checks 18 success / 16 skipped, none failed (the Governed Surface Queue Guard re-runs on the merge group); base e07eecfa behind origin/main 827cacbf with no conflict, so no base merge is taken — the queue builds the group on the current main. The approver flipped the PR ready at 2026-09-15T14:54Z and enqueued it in the same stroke (added_to_merge_queue at 2026-09-15T14:54Z; the queue ref read 1), so the seat's strokes reduce to clearing needs-user-decision (done through label-write.mjs, read back) and reading the enqueue event — no ccr/auto_merge PUT was needed. The landing is confirmed by the three readings (merged_at + git log origin/main carrying (#18276) single-parent + the queue ref gone) at the next wake, then #18218's residue is stripped and the landing record posted there; PR #18285 (#18281, serial behind this deny entry) then takes origin/main through update-branch, and #18280 becomes dispatchable.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Maintainer reply on record (director seat, 2026-09-15T15:05Z): batch #135 presented this PR as a governed draft row — 「改了什么:deny 表加 mcp__github__update_pull_request + 两行改路 / 为什么:声明关闭的写通道实际开着 / 席位意见:skills 席 PASS,17 项门禁全 0 / 一个动作:在 PR 上点 Approve」 — and the maintainer answered 「135 同意」. Review is already requested from hotlong and os-zhuang; the approval itself is the maintainer's click. The sibling ruling on #18282 (class-1, A) adds the two auto-merge tools to the same deny list in its own PR, ⛔ not folded in here. ⛔ This seat does not approve, arm or merge a governed PR.


Generated by Claude Code

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 size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants