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

docs(agents,settings): port the write-identity locks — deny MCP content writes, §9 states ruling C (objectui#9418)#9448
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_requestenable_pr_auto_mergeactions_run_triggerresolve_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.mdgit 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.jsonpermissions.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
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