Skip to content

docs(agents,settings): port the write-identity locks — deny MCP content writes, §9 states ruling C (objectui#9418) - #9448

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-9418-write-identity-locks-port
Sep 14, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-9418-write-identity-locks-port

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #9418

⛔ GOVERNED — this pull request is a DRAFT and stays one. Both changed paths are governed surface in this repository. Asked of the guard itself, in this worktree:

$ node scripts/check-governed-queue-guard.mjs --test .claude/settings.json AGENTS.md
⛔ GOVERNED — 2 of 2 path(s) are on a governed surface:
   .claude/** x1 — the agent instruction tree (skills, hooks, settings)
     - .claude/settings.json
   AGENTS.md x1 — the repo-root agent instruction file
     - AGENTS.md
   One governed path governs the WHOLE pull request — proportion is not a question.
(exit 3)

⇒ ⛔ never flipped ready, ⛔ never enqueued, ⛔ never armed with auto-merge, ⛔ never approved by a seat under any account. Under ruling C — which is the rule this pull request writes into §9 — the claiming seat lands it only once an authorized APPROVED review exists (GOVERNED_APPROVERS: os-zhuang / hotlong).

维护者速读(草稿)

改了什么

两件,都是把 objectstack 已经落地的规则搬到本仓:

  1. .claude/settings.json 新增一个 permissions.deny,点名 14 个会写内容的 MCP GitHub 工具。
  2. AGENTS.md §9 受管面段:落地动作由「停在 draft,等人类合并」改为「停在 draft,等一条获授权的批准,然后由认领席落地」(裁决 C)。

为什么改

第 1 条防的是一次已经发生的事故,不是假想。MCP 写入署名的是 claude.ai 账号绑定的那个 GitHub 用户;而一个被封的用户账号,会把它写过的一切一起藏起来 —— 评论、卡片,对所有读者 404。改走 REST 代理、署名 claude[bot](应用身份)之后,工作不再随某一个人的账号状态消失。这是对整类失败的结构性修复,不是配置整洁。

第 2 条是让本仓的规程和已生效的裁决一致。原文写的是「人类的那次合并动作本身就是审核记录 —— 不需要额外的逐 PR 批准点击,也别去等一个不存在的 approval」;而现在恰恰有一条批准要等,批准之后落地是席位的事。规程与裁决不一致时,照规程做的席位会做错事 —— 而这一段正是后续每一次 dispatch 都要读的那段。

风险与代价(含回滚)

  • 第 1 条:任何还在用 MCP 写工具的流程会被拒绝。这是有意的。四个不写内容的状态类工具(update_pull_request、enable_pr_auto_merge、actions_run_trigger、resolve_review_thread)保持放行,落地路径因此不受影响。
  • 第 2 条:受管面 PR 现在需要一条来自 os-zhuang / hotlong 的批准才能落地 —— 比原来多一个批准动作,少一个人工合并动作。⚠️ 一项已知并已被接受的代价(§9 原本就记着):批准之后的 push 不再被那道门重审。本 PR 不改变这一点。
  • 回滚:两个 commit 互相独立,git revert 任意一个都不影响另一个。没有数据迁移,没有构建产物,没有已发布包的字节变化。

席位意见

(留空,交席位定稿)

你要做的

读一下 §9 那一段的新措辞是不是你要的意思(尤其是「获批后认领席落地」这半句),然后在本 PR 上留一条 APPROVED review。批准之后由认领席翻 ready、入队落地,你不需要自己合并。

Premise readings

All four checked against the tree before anything was written; all four hold.

# premise reading when
P1 objectui .claude/settings.json has allow + hooks but no permissions.deny permissions keys on origin/main efc1c9c = ['allow'] only; the port script refuses outright if a deny key is already there 2026-09-14T00:02Z
P2 §9's hard-rule line still states a human MERGE as the landing action line 508 on origin/main began 「⛔ 绝不 gh pr ready」 and ended 「这类 PR 停在 draft,等人类合并……也别去等一个不存在的 approval。」 2026-09-14T00:03Z
P3 objectstack origin/main carries the landed deny list git log origin/main --oneline -20 | grep -c '(#18072)' = 1, at 7ef05f997 — so the final spelling exists and the card's Blocked-by: is cleared 2026-09-14T00:01Z
P4 objectui PR #9301 touches AGENTS.md in a different section its AGENTS.md hunk is @@ -107,6 +107,13 @@ (§5 Coding Standards, adding a cite-by-content commandment), +7 −0. This branch's hunks are at 318 / 483 / 502 / 508 / 517, all in §9. Zero overlap; that section is untouched here 2026-09-14T00:06Z

Item 1 — the deny list, verified

⛔ Never retyped. A script parses both JSON documents and moves the array object across; it refuses rather than writing a partial list. A round-trip check ran first, so the rewrite provably alters nothing but the inserted key:

orig bytes: 1305 sha: fb32f4399247ed5e
roundtrip bytes: 1305 sha: fb32f4399247ed5e
ROUND-TRIP IDENTICAL: True

The two deny arrays are identical (14 = 14):

$ diff <(objectstack origin/main .claude/settings.json -> permissions.deny, one per line) \
       <(this branch .claude/settings.json    -> permissions.deny, one per line)
diff exit=0 (0 = identical)

objectstack deny: 14
objectui    deny: 14

The file parses, on both readers that matter:

json.load OK
node JSON.parse OK

allow and hooks are byte-identical to origin/main:

permissions.allow  identical: True  sha(origin/main)=47db024792d3a3c1 sha(branch)=47db024792d3a3c1
hooks              identical: True  sha(origin/main)=11e86a5246b857a6 sha(branch)=11e86a5246b857a6
new keys in permissions vs origin/main: ['deny']
keys removed: []

The diff is purely additive: 1 file changed, 16 insertions(+), with the only touched pre-existing byte being the ] that becomes ], where allow closes. Nothing else from objectstack's file came across — its hooks and its allow list stayed there.

Fence 2 held — the four state-shaped tools are absent, with a positive control so the zeros are a reading rather than a hopeful grep:

update_pull_request      in deny: 0
enable_pr_auto_merge     in deny: 0
actions_run_trigger      in deny: 0
resolve_review_thread    in deny: 0
(positive control) issue_write in deny: 1

Consistency edits within §9

Net 0 lines in AGENTS.md — git diff --numstat reports 5 5 AGENTS.md, and every hunk is @@ -N +N @@, i.e. one line replaced by one line at 318, 483, 502, 508 and 517. No other line in the file moves.

人类合并 appeared exactly 6 times in §9 before this change. Five are below. The sixth, on line 522, is deliberately left alone: it describes what scripts/check-governed-queue-guard.mjs prints, not what the rule is — and the script does still print that remedy, as the --test output quoted at the top of this body shows.

1. Line 508 — the hard rule (the equal-line replacement). Stated in objectstack lanes/ui.md :25's own words; ⛔ not paraphrased.

before

⛔ 绝不 gh pr ready(不退出 draft)、⛔ 绝不加入合并队列、⛔ 绝不 gh pr merge --auto / enable_pr_auto_merge、⛔ 绝不自己合并。这类 PR 停在 draft,等人类合并。人类的那次合并动作本身就是审核记录 —— 不需要额外的逐 PR 批准点击,也别去等一个不存在的 approval。

after

⇒ 命中即停 draft;⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地。「获授权批准」= GOVERNED_APPROVERS(os-zhuang / hotlong)里某个账号的一条 latest-decisive APPROVED review,DISMISSED 与被顶掉的不算(完整判据在下面那段);在它出现之前 ⛔ 绝不 gh pr ready(不退出 draft)、⛔ 绝不加入合并队列、⛔ 绝不 gh pr merge --auto / enable_pr_auto_merge、⛔ 绝不自己合并。那条批准本身就是审核记录 —— 它出现之后,gh pr ready + gh pr merge --squash --auto --delete-branch 由认领席执行,照常走合并队列(维护者 2026-09-13 裁决 C,原文照录、不翻译:「C. approve 后不管后续改动都由席位落地:」)。

The bolded clause is lanes/ui.md :25 verbatim. The four concrete prohibitions are kept and re-anchored to "before that approval", because they are the operational half a reader acts on; what leaves is the two now-false sentences — that a human's merge is the review record, and that there is no approval to wait for.

2. Line 318 — the exception clause inside the 「CI 全绿即自行合并」 bullet. Only the parenthesised clause moves; the rest of that long bullet is untouched.

  • before: 那类 PR 停在 draft 等人类合并)
  • after: 那类 PR 停在 draft 等一条获授权的批准,再由认领席落地)

3. Line 483 — the subsection heading.

  • before: ### ⛔ 受管面(governed surface):agent 起草,人类合并
  • after: ### ⛔ 受管面(governed surface):agent 起草,获授权批准后由认领席落地

Checked before changing it: nothing in the tree links to this heading by anchor — the only occurrence of the heading text anywhere is the heading itself.

4. Line 502 — the skills/** bullet.

  • before: 停在 draft 等人类合并,⛔ 不翻 ready、不入队。
  • after: 停在 draft 等一条获授权的批准,⛔ 批准前不翻 ready、不入队。

5. Line 517 — the mixed-diff bullet.

  • before: 整个 PR 等人类合并。
  • after: 整个 PR 等那条获授权的批准。

Untouched on purpose: the fifth prohibition (line 510) and the two verbatim maintainer quotes (512, 522). Verified mechanically — 第五条禁令 and An agent seat never submits appear in the diff only as context lines, never on a + or - line.

Gates

Derived from this repo's own package.json scripts and the .github/workflows/*.yml that name .claude/ or AGENTS.md, then widened by asking which scripts/*.mjs actually read the changed paths — which is how check:shell-escape-residue and check:doc-links got onto the list; neither was named up front, and both read AGENTS.md directly.

gate exit verdict line
check:governed-queue-guard 0 OK check-governed-queue-guard self-test: 185 cases pass
check:control-bytes 0 ✅ check-control-bytes: OK (scanned 7617 tracked text file(s); skipped 85 binary).
check:skills-paths 0 ✅ check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).
check:shell-escape-residue 0 ✅ check-shell-escape-residue: OK (5/5 root(s) resolved -- AGENTS.md: 1 file(s), 15 fence(s); …)
check:doc-links 0 Links are valid across 17 scan roots.
check:new-line-citations 0 VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0
check:changeset-claims 0 report-only; no finding against this diff
check-changeset-presence 0 ✅ No source or published contract of a released package changed in this range, so no changeset is owed.
hook self-tests (all 4) 0 130 passed · 120 passed · 50 passed · 38 passed — 338 cases, 0 failed

Hook wiring was checked as well, since settings.json is what names the hooks: all 4 commands it wires resolve to files on disk.

lint and type-check — a declared, proven narrowing rather than a repo-wide run. Three readings, taken from the instruments rather than asserted:

  1. Population, read from eslint's own config (not guessed): every rule block is scoped to TypeScript — the distinct files globs are **/*.{ts,tsx}, scripts/**/*.{ts,tsx}, **/examples/**/*.{ts,tsx}, **/*.test.{ts,tsx}, **/__tests__/**/*.{ts,tsx}, packages/cli/src/**/*.{ts,tsx}, packages/create-plugin/src/**/*.{ts,tsx}, **/*.tsx, packages/types/src/objectql.ts, packages/core/src/registry/Registry.ts. There is no markdown or JSON block.
  2. Count, read from --format json: eslint run over the three changed files reports 3 files, 0 errors.
  3. Invariance for untouched files: type-aware linting is not enabled anywhere — no block sets parserOptions.project or projectService — so this diff cannot move the verdict on any file it does not touch. Independently: git diff --name-only over this branch returns 0 files with a .ts / .tsx / .js / .mjs / .cjs extension. The same reading covers type-check: tsc's inputs are unchanged, byte for byte.

All readings above were taken on 660ab2c, the final commit of this branch.

The repo-wide pnpm lint and pnpm type-check farms remain CI's run, not this branch's.

Acceptance notes

Observations from the surrounding text, ⛔ none of them acted on here — this is a governed rules-layer paragraph and a seat should not widen the edit on its own judgement.

  • scripts/check-governed-queue-guard.mjs still prints the old landing shape. Its refusal text reads Park it as a DRAFT and leave the merge to the maintainer; a human merge IS the review record for a governed surface — visible verbatim in the --test output at the top of this body. After this pull request, AGENTS.md states ruling C while the script a seat is told to consult still narrates draft-and-human-merge. The card scopes to .claude/settings.json + AGENTS.md, so the script is out of scope here; it is reported to the PM to file. Worth noting the divergence is one-directional and harmless in the safe direction — the script under-permits rather than over-permits.
  • Line 519 — 「CI 全绿、已 review 都不构成例外。」 — left exactly as it is. It does not state the landing action as a human merge, so it is outside the four-line criterion this pull request was scoped to. But 「已 review」 now sits close to the thing that is the exception under ruling C: an authorized APPROVED review. The sentence that follows it keeps it defensible (its subject is green CI not justifying policy), so it reads as coherent rather than contradictory — flagging it so the approver can rule, not proposing a change.
  • Line 522 left alone deliberately — it describes the guard's behaviour and printed remedy, not the rule. Changing it would make the paragraph describe a script that does not exist yet.
  • The changeset is declared, not owed. check-changeset-presence says on its own terms that no changeset is owed (nothing released moved). The empty-frontmatter declaration is written anyway, because this repo's rule asks for a declaration once rather than for a release, and because PR docs(agents): cite by content, not by line address (#8875 clause 1) #9301 — the nearest precedent, an AGENTS.md-only change — carries exactly that shape. ⛔ No skip-changeset label is applied: in this repo that label is read by nothing and exempts nothing.
  • PR docs(agents): cite by content, not by line address (#8875 clause 1) #9301 is not affected by this branch and this branch is not affected by it — different section of AGENTS.md, verified above. Separately noted, not acted on: its hunk adds a Coding Standard numbered #10, and a commandment #10 has since landed on origin/main from another card, so that branch has two #10s to reconcile when it merges main. That is that seat's PR to sort out; recorded here only because reading its diff was a required step and the collision was visible while doing so.

Clause-②: no

Session, as prose so it survives a body rewrite: this branch was produced in session_01DAcomhvR9kKizeYgg89Vo8.


Generated by Claude Code

…i#9418)

Write-identity lock 1, ported from objectstack `origin/main` (`7ef05f997`).
Every content write from a seat or a dev agent now has to leave through the
REST proxy, where it is authored by the app identity `claude[bot]` instead of
the claude.ai account's linked GitHub user.

The failure mode this closes is not hypothetical and not about tidiness: a
suspended user account hides everything it authored. When that happened on
this fleet the losses were a triage seat's round comments, cards that went
404 to every reader, and a shift closed out over it — work that existed,
was correct, and simply stopped being visible. An app identity cannot be
suspended the way a user account was, so work authored by it does not vanish
with one account's status.

The list is copied verbatim by a script that parses both JSON documents and
moves the array object across — never retyped. A hand-copied deny list that
is one entry short is a hole that nothing reports: the missing tool keeps
working, under the wrong identity, and no gate and no reader can tell.

The four state-shaped tools stay allowed on purpose (`update_pull_request`,
`enable_pr_auto_merge`, `actions_run_trigger`, `resolve_review_thread`):
they author no content, so denying them buys no identity benefit, and the
landing path needs them.

Nothing else crosses over — objectstack's own hooks and allow list stay
there. This repo's allow list and hooks are byte-identical to `origin/main`;
the diff is purely additive.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
…, then the seat lands (objectui#9418)

Write-identity lock 5. §9's governed-surface paragraph said a governed PR
stops at draft and waits for a HUMAN MERGE, and that the human's merge action
is itself the review record — so there was no approval to wait for, and the
paragraph said so in as many words.

Ruling C moved the landing action without moving the restraint: a governed hit
still stops at draft, but what releases it is an authorized APPROVED review
(`GOVERNED_APPROVERS` — os-zhuang / hotlong — latest-decisive, not dismissed),
after which the CLAIMING SEAT lands it through the merge queue. The restraint
is unchanged in substance and the fifth prohibition is unchanged in text: a
seat never leaves that approval itself, under any account.

The hard-rule line is replaced equal-line, and states the rule in objectstack
`lanes/ui.md`'s own words rather than a paraphrase of them. That is not
style: this paragraph is the operating procedure every later dispatch reads,
and two wordings of one rule drift apart silently — the repo has already paid
for that once, with the `skills/**` clause that was written backwards here
while the guard read it the other way.

Four other lines named the landing action as a human merge — the exception
clause in the "CI 全绿即自行合并" bullet, the subsection heading, the
`skills/**` bullet, and the mixed-diff bullet. Each gets the minimal word
change, because a paragraph that states the new rule once and the old rule
four more times is worse than either rule alone.

The line describing what `scripts/check-governed-queue-guard.mjs` PRINTS is
deliberately left alone: it describes the script, not the rule, and the script
still prints the draft-and-human-merge remedy. That divergence is real and is
called out in the pull request rather than quietly papered over here.

Net 0 lines; no other line in the file moves. The changeset declares no
release with an empty frontmatter — neither path is released package source.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author
  • Served-tier: 1550/1550 CONTRACT_REVIEW_TIER — harness model stamp counted over this seat's own transcript (non-sidechain assistant messages a model served; <synthetic> harness notices excluded) at 2026-09-14T00:19Z and compared to the constant's value outside the repository; get_session external_metadata.last_served_model read equal to the constant at 2026-09-14T00:20Z. Identifier-free form per objectstack AGENTS.md's comment rule (objectstack#18060); this repository's guard reads no Served-tier: line.

Contract review

Head: 660ab2ce (PR #9448, card #9418) — reviewed at 2026-09-14T00:22Z by the skills seat at the contract-review tier. GOVERNED rules layer in this repository (.claude/settings.json + AGENTS.md; guard --test exit 3, 2 of 2) ⇒ this record is the seat's ACCEPT, not the landing: the draft stays until an authorized APPROVED review (os-zhuang / hotlong, not dismissed) exists; then this seat lands it under ruling C.

① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):

  1. Item 1 — the deny list is byte-for-byte objectstack's: diff of the two permissions.deny arrays (objectstack origin/main 7ef05f997 vs this head) is empty, 14 = 14; permissions.allow and hooks are byte-identical to objectui origin/main; the diff is purely additive (+16 / −0); the four state-shaped tools are absent from the list (control: issue_write present).
  2. Item 2 — AGENTS.md net 0 lines (578/578), five 1:1 replacements at lines 318 / 483 / 502 / 508 / 517 and nothing else: line 508 carries lanes/ui.md :25 verbatim in bold (命中即停 draft;⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地), defines 「获授权批准」 by the guard's own criterion, keeps the four operational prohibitions anchored to 「在它出现之前」, and quotes ruling C verbatim (「C. approve 后不管后续改动都由席位落地:」); the four consistency edits change only the words that named a human merge; line 522 (which describes what the guard prints) and the fifth prohibition with its verbatim quotes are unchanged — 人类合并 6 → 1, the survivor being 522.
  3. Scope held: three files — the two governed paths and an empty-frontmatter changeset; no other line moved; PR docs(agents): cite by content, not by line address (#8875 clause 1) #9301's AGENTS.md hunk (a Coding Standard) is not touched.
  4. Gates: the dev derived its list from objectui's own package.json + workflows and widened it by which scripts read the changed paths — 8 gate rows + 4 hook self-test matrices, all exit 0, quoted in the PR body; on the head at 2026-09-14T00:19Z: 35 checks — 23 green, 3 skipped, 9 running, 0 red; Governed Surface Queue Guard green on the pull_request leg (early warning by design).
  5. Clause-②: no holds — no contract path; declared and enforced agree.

② semver: unchanged — nothing released moved; the empty-frontmatter changeset declares no release (check-changeset-presence: no changeset owed).

③ boundary flags for the approver: (a) line 519 「CI 全绿、已 review 都不构成例外。」 is unchanged — 「已 review」 now sits next to the one thing that IS the exception under ruling C (an authorized approval); the sentence after it keeps its subject green CI, so it reads coherent; the approver rules whether to touch it (⛔ not widened here). (b) scripts/check-governed-queue-guard.mjs still prints the pre-ruling-C remedy (leave the merge to the maintainer; a human merge IS the review record) — a class (b) divergence, filed as #9449 (and objectstack#18083 for the objectstack copy); it under-permits, so it fails safe.

Implemented-by: claude/issue-9418-write-identity-locks-port
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

Verdict: PASS — awaiting an authorized approval; ⛔ never ready / enqueue / auto-merge before it; the seat lands after.

维护者速读(终稿)

objectui 补上 objectstack 昨天落地的两道锁的本仓半边:① .claude/settings.json 加 permissions.deny,逐字节复制 objectstack 那 14 个会写内容的 MCP GitHub 工具,以后本仓所有席位和 dev 的写入都走 REST 代理、作者是 claude[bot],账号被封也不会再丢内容;四个只改状态的工具(un-draft、auto-merge、重跑 CI、resolve thread)保留。② AGENTS.md §9 受管面那段把「停在 draft 等人类合并」改成裁决 C 的原话:未获授权批准前不 ready 不入队不自合、永不批准,获批后由认领席落地;其余四处提到「人类合并」的句子同步改词,段落不再自相矛盾;第五条禁令和你的原话引文一字未动;行数不变。需要你或 hotlong 一条 APPROVED review,之后本席落地。附带两点:第 519 行「已 review 都不构成例外」要不要顺手改,由批准者定;守卫脚本打印的补救话术仍是旧形态,已另立卡 #9449 / objectstack#18083。


Generated by Claude Code

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

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Ruling C landing step — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-14T03:38Z. An authorized approval exists on this governed PR: os-zhuang, review 5193668433, APPROVED on the current head 660ab2ce at 2026-09-14T03:35Z, not dismissed (GOVERNED_APPROVERS in this repository's guard: os-zhuang / hotlong). The maintainer's own hand also marked the PR ready and it entered the merge queue at 2026-09-14T03:35Z (queue ref gh-readonly-queue/main/pr-9448-* present at 2026-09-14T03:37Z; mergeable_state: clean). Seat readings: the review of record 5657351723 names this head — no push since, so it stands without re-issue; needs-user-decision cleared at 2026-09-14T03:37Z through scripts/pm/label-write.mjs (four steps, directed DELETE, read-back: labels documentation only); ready and the queue were the maintainer's act, so nothing to arm. Landing record follows on #9418 once origin/main carries (#9448).


Generated by Claude Code

Merged via the queue into main with commit 3c6b09a Sep 14, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9418-write-identity-locks-port branch September 14, 2026 03:54
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ude/settings.json (objectui#9586) (objectstack-ai#10214)

Fixes objectstack-ai#9586

⛔ **GOVERNED — this pull request is a DRAFT and stays one.** The changed
instruction-tree path is governed surface in this repository. Asked of
the guard itself, in this worktree:

```
$ node scripts/check-governed-queue-guard.mjs --test .claude/settings.json .changeset/issue-9586-enqueue-class-deny-mirror.md
⛔ GOVERNED — 1 of 2 path(s) are on a governed surface:
   .claude/** x1 — the agent instruction tree (skills, hooks, settings)
     - .claude/settings.json

   One governed path governs the WHOLE pull request — proportion is not a question.
   ⛔ Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT and leave the merge
      to the maintainer; a human merge IS the review record for a governed surface.
   The merge-queue run of "Governed Surface Queue Guard" refuses this diff unless an APPROVED review by an
   authorized approver (GOVERNED_APPROVERS: os-zhuang, hotlong) is on the pull request — on
   whichever commit it was left (maintainer ruling 2026-09-04).
(exit 3)
```

⇒ ⛔ never flipped ready, ⛔ never enqueued, ⛔ never armed with
auto-merge, ⛔ never approved by a seat under any account. Under ruling C
the claiming seat lands it only once an authorized APPROVED review
exists.

## 维护者速读(草稿)

**改了什么**

一个文件、三行:`.claude/settings.json` 的 `permissions.deny` 从 14 条加到 17
条,新增点名三个 MCP GitHub 工具 ——
`update_pull_request`、`enable_pr_auto_merge`、`disable_pr_auto_merge`。改完之后,本仓的
deny 数组与 objectstack 的**逐元素相同、顺序也相同**。`allow` 与 `hooks` 两块字节未动。另附一个空
frontmatter 的 changeset(声明「不发版」,本仓惯例)。

**为什么改**

这是 objectstack 已裁决条款的跨仓镜像,不是本仓的新主张。objectstack#18282 裁决 A(批次 objectstack-ai#135
追认)把「声明即强制」扩到**入队类**:席位在两个板上真正用的入队通路只有 REST 的 `PUT
.../pulls/{n}/ccr/auto_merge`(它就在同一个文件的 `allow` 里),所以把 MCP
的那两个入队工具关掉,**不损失任何能力**,只是把一条没人走的旁路封死。

第三条 `update_pull_request` 是本仓自己的事故教训:objectui#6183 里,一次只传 `reviewers` 的
MCP 调用**顺带**把 PR 的 `draft: false` 设了,于是一个受管草稿进了合并队列、零人工批准就落了地。本仓当时只镜像了
14 条内容写入类,这一条(objectstack 在 PR #18276 落地)和入队那两条(PR #18317)都没跟上 ——
本次一并补齐。

**风险与代价(含回滚)**

- 代价:任何还想用 MCP 去翻 ready、挂/摘 auto-merge 的流程会被运行时拒绝。这是有意的。`gh pr
ready`、`gh pr merge --auto` 与 REST 的 ccr 路径都不受影响。
- ⚠️ 已知代价,需要你知道:AGENTS.md 里有两处**配方文本**仍然点名这两个工具(自行合并那条的「MCP
等价物」一行,以及受管面段「发现自己已经挂上了怎么办」那条补救 —— 后者写的是「只调 `disable_pr_auto_merge` ……
两个都要做」)。本卡的文件面只有 `.claude/settings.json`,且 AGENTS.md 当前被两个在途 PR
持有(objectstack-ai#10212 / objectstack-ai#10213),所以本 PR ⛔
不碰它,改由席位另立卡。方向是安全的一侧:配方失效会**响亮地被拒**,不会静默做错事。
- 回滚:`git revert` 单个 commit 即可,无数据迁移、无构建产物、无已发布包字节变化。

**席位意见**

(留空,交席位定稿)

**你要做的**

确认「入队类 MCP 工具一并关掉」这个方向是你要的(objectstack 那边已经这么定了),然后在本 PR 上留一条 APPROVED
review。批准之后由认领席翻 ready、入队落地,你不需要自己合并。上面那条 ⚠️ 只需要你知道,不需要你在这张卡里处理。

## 前提核验(动手前,在 `origin/main` `0cf2d6644` 上实测)

| # | 前提 | 读数 | 结论 |
|:--|:--|:--|:--|
| P1 | 卡片说本仓 deny 有 14 条 `mcp__github__*`,且三条都不在 | 实测 deny 总数 14,全部是
`mcp__github__*`;三条各命中 0 次,正控 `issue_write` 命中 1 次 | 成立 |
| P2 | 本仓有没有 deny 名册门禁(objectstack 的
`scripts/pm/check-settings-deny-roster.mjs`) | 不存在;`scripts/pm/` 全部内容是
`board-snapshot.mjs` 与 `check-half-states.mjs`;全仓 grep
`settings.json`,`scripts/` 与 `.github/` 内零个读取者 | 无 ⇒ ⛔ 不移植该门禁 |
| P3 | 本仓有没有通道对照表(objectstack 的 `references/rest-channel.md`) |
`.claude/` 下不存在任何 `references` 目录;`.claude/` 全部文件共 14 个(4 个 hook + 4 个
selftest + `launch.json` + `settings.json` + 2 个 skill 的 4 个 md) | 无 ⇒
那一行事实无处可加 |
| P4 | `.claude/settings.json` 是不是被 pin 的上游移植文件(改它会红
`check:upstream-port-parity`) | 不是。`scripts/upstream-port-pin.json` 13
条,`.claude/` 下只 pin 了 8 个 hook/selftest 文件 | 可以改;该门禁仍已跑,绿 |
| P5 | 并发:有没有别的在途 PR 碰这个文件 | 7 个 open PR 的文件清单逐一读过,零个碰
`.claude/settings.json`;`objectstack-ai#10212` / `objectstack-ai#10213` 只有 `AGENTS.md`(hunk 分别在 468
与 323 行区) | 无冲突 |

## 执行判据(卡片的那一条)

推送之后,`GET
/repos/objectstack-ai/objectui/contents/.claude/settings.json?ref=claude/issue-9586-settings-deny-auto-merge-mirror`
解码后的 `permissions.deny`(HTTP 200,blob sha
`30c3367037de7ba000673e4da8caa2913a9c704c`,size 2255):

```
 1 mcp__github__issue_write
 2 mcp__github__create_pull_request
 3 mcp__github__update_pull_request          ← 新增(objectstack PR #18276 的镜像)
 4 mcp__github__add_issue_comment
 5 mcp__github__add_comment_to_pending_review
 6 mcp__github__add_reply_to_pull_request_comment
 7 mcp__github__pull_request_review_write
 8 mcp__github__push_files
 9 mcp__github__create_or_update_file
10 mcp__github__delete_file
11 mcp__github__create_branch
12 mcp__github__sub_issue_write
13 mcp__github__merge_pull_request
14 mcp__github__create_repository
15 mcp__github__fork_repository
16 mcp__github__enable_pr_auto_merge         ← 新增(本卡的判据)
17 mcp__github__disable_pr_auto_merge        ← 新增(本卡的判据)
```

两条判据条目都在 ⇒ 判据满足。

## 改动的形状,实测而非声称

⛔ 没有手打重排。两处锚点文本替换,锚点各自唯一,其余字节不动:

```
deny  base: 14  branch: 17  objectstack: 17
branch deny === objectstack deny (order included): true
allow identical to origin/main: true   sha 46560076ce2be885 = 46560076ce2be885
hooks identical to origin/main: true   sha 10e6c3a3ae991607 = 10e6c3a3ae991607
top-level keys / permissions keys: 与 origin/main 相同,无增删
added:   ["mcp__github__update_pull_request","mcp__github__enable_pr_auto_merge","mcp__github__disable_pr_auto_merge"]
removed: []
duplicate entries: 0
git diff --numstat: 4 1 .claude/settings.json
```

顺序惯例是**按类分组、不是字母序**(实测:objectstack 把 `update_pull_request` 紧挨
`create_pull_request`、把入队对追加在末尾),本 PR 照同样的位置插入,结果两仓数组完全一致。

两个读者都能解析:`node JSON.parse OK` · `python json.load OK`。`settings.json` 里
wire 的 4 个 hook 命令全部落在磁盘上(4/4 OK)—— 这个文件正是点名它们的那一个。

## 门禁

门禁族由本仓自己推导:objectui 没有 `scripts/pm/dispatch-gates.mjs`(不存在),所以逐个读
`.github/workflows/*.yml` 的触发条件,取「会判到这两个路径」的全集。

| 门禁 | exit | 判定行 |
|:--|:--|:--|
| JSON 有效性 | 0 | `node JSON.parse OK` · `python json.load OK` |
| `check-control-bytes`(commit 后重跑) | 0 | `✅ check-control-bytes: OK
(scanned 8217 tracked text file(s); skipped 85 binary).` |
| `check-governed-queue-guard --self-test` | 0 | `OK
check-governed-queue-guard self-test: 185 cases pass` |
| `check-governed-queue-guard --test` | 3 | 本文顶部那段(受管,exit 3 是本 PR
的健康读数) |
| `check-changeset-presence` | 0 | `✅ No source or published contract of
a released package changed in this range, so no changeset is owed.` |
| `check-changeset-no-major` | 0 | `✅ No changeset declares a major
bump.` |
| `check-changeset-overwrite` | 0 | `✅ No pre-existing changeset was
modified or deleted.` |
| `check-lint-coverage` | 0 | `✅ lint coverage: 46/46 packages linted, 0
with outstanding errors (0 total).` |
| `check-entry-guard --self-test` / 全跑 | 0 / 0 | `63 cases pass` · `108
scripts/ file(s) — no entry guard outside the baseline` |
| `check-upstream-port-parity --self-test` / 全跑 | 0 / 0 | `66 cases
pass` · `13 ported file(s) match objectstack-ai/objectstack modulo their
declared divergences` |
| `check-bash32-floor --self-test` / 全跑 | 0 / 0 | `160 cases pass` · `13
tracked shell file(s) … name no bash 4+ construct` |
| `check-skills-paths` | 0 | `✅ check-skills-paths: OK (88/89 stated
path(s) resolve across 20 guide file(s); 1 baselined).` |
| `check-new-cross-file-line-citations` | 0 | `VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0` |

退出码一律在任何管道之前捕获(先重定向到文件,再读 `$?`)。

**不欠的两族,连同不欠的理由:** `hook-selftests.yml` 的 paths 是 `.claude/hooks/**`
与它自己 —— 本 diff 不碰 hook 文件(hook 命令仍被 `settings.json` 点名,所以单独验了 4/4
落盘)。`check-shell-escape-residue` 的 `SCAN_ROOTS` 是
`AGENTS.md`、`CLAUDE.md`、`skills`、`.claude/skills`、`content/docs`
五条,`.changeset/**` 不在其中 ⇒ 新增的 changeset 不在它的总体里。

**lint 与 type-check:一次申报过的收窄,不是一次漏跑。** 三条读数里的两条来自仪器,第二条被替换,如实写明:

1. **总体读自 eslint 自己的配置**:`eslint.config.js` 里每一个 `files` 块都限定在
TS/TSX(`**/*.{ts,tsx}`、`scripts/**/*.{ts,tsx}`、`**/*.test.{ts,tsx}`、`**/__tests__/**/*.{ts,tsx}`、`**/examples/**/*.{ts,tsx}`、`**/*.tsx`
及三个具名单文件),没有 JSON 块,也没有 markdown 块。
2. **文件数**:⚠️ 本次**不是**读自 `--format json`,而是读自 diff 的扩展名普查 —— 本分支 `git
diff --name-only` 两个文件,`.json` 与 `.md` 各一,`.ts/.tsx/.js/.mjs/.cjs` 命中
**0** ⇒ 落在 eslint 总体里的文件数必然为 0。这个容器里 objectui 没有 `node_modules`(共享检出与本
worktree 均无),而为一个 JSON + markdown 的 diff 做一次全仓 `pnpm install`
是这台共享机器上最贵的动作、且买不到任何判定。⇒ 这一格的仪器读数记 NOT MEASURED,理由如上;推翻它只需要一个 `.ts`
文件,而本 diff 没有。
3. **对未触碰文件的不变性**:type-aware linting 全仓未启用(`eslint.config.js` 里没有任何块设
`parserOptions.project` 或 `projectService`)⇒ 本 diff
移不动任何它没触碰的文件的判定。同一读数覆盖 `type-check`:tsc 的输入字节未变。

仓级 `pnpm lint` / `pnpm type-check` 农场是 CI 的运行。⚠️ 并且它**会**在本 PR
上跑满:`lint.yml` 的相关性判断只排除
`**/*.md`、`content/**`、`docs/**`、`.changeset/**`,而
`.claude/settings.json` 不在排除列里 ⇒ `should_run=true`,安装之后的那半边门禁由 CI 如常判定。

上述读数取自本分支最终 commit `45f22049`。

## Acceptance notes

顺路看到、⛔ 本 PR 一律没动的:

- **AGENTS.md 有两处配方点名了本 PR 刚关掉的工具。** 一处在「CI 全绿即自行合并」那条的代码块里(`# MCP
等价物:pull request update(draft: false) +
enable_pr_auto_merge`),一处在受管面段的补救条(「只调 `disable_pr_auto_merge` 会摘掉
auto-merge 但不取消队列成员资格,两个都要做」)。本 PR 落地之后,照这两处文本行事的席位会撞上运行时拒绝。objectstack
那边的对应修复是改它的通道对照表那一行(「auto-merge 备用 MCP 未拒」→「auto-merge MCP 锁 1
同拒」);本仓的等价文本恰好落在 AGENTS.md 里,而 AGENTS.md **不在本卡的文件面内**,且当前被 objectstack-ai#10212 /
objectstack-ai#10213 两个在途 PR 持有。⇒ 交席位另立卡,本 PR 不碰。方向安全:失效的配方会被响亮拒绝,不会静默做错。
- **`.changeset/issue-9418-write-identity-locks-port.md` 里有一句已经过时的话。**
那份(空 frontmatter、永不消费的)历史声明写着四个「状态形」工具 —— 含 `update_pull_request` 与
`enable_pr_auto_merge` —— 「stay allowed」。本 PR 之后其中两个不再
allowed。它是一份历史记录而非规程,读者为零,⛔ 不改、也不建议为它单独立卡;记在这里只是为了下一个读到那句话的人不被它误导。
- **`check-governed-queue-guard.mjs` 的拒绝文案仍写着「a human merge IS the
review record」**,而 AGENTS.md 已按裁决 C 改成「获授权批准后由认领席落地」。这条在 objectui PR
objectstack-ai#9448 的 Acceptance notes 里已经报过一次,至今仍在;本 PR 只是再次撞见,不重复立卡。

`Clause-②: no`

本次实现所在的会话,写成散文以便在正文被改写后仍可追溯:`session_01Wnstp2kTth7sGXfr8fXypc`。

## 席位意见(`domain:skills` seat 1,session
`session_01Wnstp2kTth7sGXfr8fXypc`)

**建议:批准并由席位落地。** 一个文件三行,把 objectui 的 `permissions.deny` 补成与 objectstack
逐元素、同顺序相同(席位在复核工作树上解析两边数组比对为真);`allow` / `hooks`
未动;三条都在卡的裁决范围内(objectstack#18282 裁决 A、批次 objectstack-ai#135 追认;`update_pull_request`
是卡正文点名的「本仓一并缺失」项)。不损失能力:两个板上席位真正用的入队通路只有 REST `PUT
…/ccr/auto_merge`,就在同一文件的 `allow` 里。卡留给取卡者的两个可选项 —— 移植 deny 计数门禁、频道表加一行
—— 本树都不存在对应物,均未做(新增门禁默认否)。

**你需要知道的一件事(不阻塞本 PR):** 落地后 objectui `AGENTS.md` :331 与 :531 两处配方文字仍写着
`enable_pr_auto_merge` / `disable_pr_auto_merge` 作为 MCP
等价物,届时会被响亮拒绝而非静默执行错;两个在途 AGENTS.md PR(objectstack-ai#10212 :468、objectstack-ai#10213
:323)不在那两段,不能顺带。席位身份过不了立卡门,已记到席位帖的维护者事项里,由你决定是否立卡。

复核记录:PR 评论 5778587633(PASS,在席按服务档渲染);ACCEPT 在卡 objectui#9586。CI 读数:42
latest-per-name check runs — 39 success, 3 skipped, 0 in progress, 0
other。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc)_

---
_Generated by [Claude Code](https://claude.ai/code)_

Co-authored-by: Claude <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port the write-identity locks to objectui — .claude/settings.json deny list for MCP GitHub content writes, and AGENTS.md §9 stated under ruling C

2 participants