docs(agents,settings): port the write-identity locks — deny MCP content writes, §9 states ruling C (objectui#9418) - #9448
Conversation
…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
Contract reviewHead: ① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):
② semver: unchanged — nothing released moved; the empty-frontmatter changeset declares no release ( ③ 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) Implemented-by: Verdict: PASS — awaiting an authorized approval; ⛔ never ready / enqueue / auto-merge before it; the seat lands after. 维护者速读(终稿)objectui 补上 objectstack 昨天落地的两道锁的本仓半边:① Generated by Claude Code |
|
Ruling C landing step — skills seat (session Generated by Claude Code |
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:
⇒ ⛔ 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 已经落地的规则搬到本仓:
.claude/settings.json新增一个permissions.deny,点名 14 个会写内容的 MCP GitHub 工具。AGENTS.md§9 受管面段:落地动作由「停在 draft,等人类合并」改为「停在 draft,等一条获授权的批准,然后由认领席落地」(裁决 C)。为什么改
第 1 条防的是一次已经发生的事故,不是假想。MCP 写入署名的是 claude.ai 账号绑定的那个 GitHub 用户;而一个被封的用户账号,会把它写过的一切一起藏起来 —— 评论、卡片,对所有读者 404。改走 REST 代理、署名
claude[bot](应用身份)之后,工作不再随某一个人的账号状态消失。这是对整类失败的结构性修复,不是配置整洁。第 2 条是让本仓的规程和已生效的裁决一致。原文写的是「人类的那次合并动作本身就是审核记录 —— 不需要额外的逐 PR 批准点击,也别去等一个不存在的 approval」;而现在恰恰有一条批准要等,批准之后落地是席位的事。规程与裁决不一致时,照规程做的席位会做错事 —— 而这一段正是后续每一次 dispatch 都要读的那段。
风险与代价(含回滚)
update_pull_request、enable_pr_auto_merge、actions_run_trigger、resolve_review_thread)保持放行,落地路径因此不受影响。git revert任意一个都不影响另一个。没有数据迁移,没有构建产物,没有已发布包的字节变化。席位意见
(留空,交席位定稿)
你要做的
读一下 §9 那一段的新措辞是不是你要的意思(尤其是「获批后认领席落地」这半句),然后在本 PR 上留一条 APPROVED review。批准之后由认领席翻 ready、入队落地,你不需要自己合并。
Premise readings
All four checked against the tree before anything was written; all four hold.
.claude/settings.jsonhasallow+ hooks but nopermissions.denypermissionskeys onorigin/mainefc1c9c=['allow']only; the port script refuses outright if adenykey is already thereorigin/mainbegan 「⛔ 绝不gh pr ready」 and ended 「这类 PR 停在 draft,等人类合并……也别去等一个不存在的 approval。」origin/maincarries the landed deny listgit log origin/main --oneline -20 | grep -c '(#18072)'= 1, at7ef05f997— so the final spelling exists and the card'sBlocked-by:is clearedAGENTS.mdin a different sectionAGENTS.mdhunk 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 hereItem 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:
The two
denyarrays are identical (14 = 14):The file parses, on both readers that matter:
allowandhooksare byte-identical toorigin/main:The diff is purely additive:
1 file changed, 16 insertions(+), with the only touched pre-existing byte being the]that becomes],whereallowcloses. 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:
Consistency edits within §9
Net 0 lines in
AGENTS.md—git diff --numstatreports5 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 whatscripts/check-governed-queue-guard.mjsprints, not what the rule is — and the script does still print that remedy, as the--testoutput 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
after
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.
3. Line 483 — the subsection heading.
### ⛔ 受管面(governed surface):agent 起草,人类合并### ⛔ 受管面(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.5. Line 517 — the mixed-diff bullet.
Untouched on purpose: the fifth prohibition (line 510) and the two verbatim maintainer quotes (512, 522). Verified mechanically —
第五条禁令andAn agent seat never submitsappear in the diff only as context lines, never on a+or-line.Gates
Derived from this repo's own
package.jsonscripts and the.github/workflows/*.ymlthat name.claude/orAGENTS.md, then widened by asking whichscripts/*.mjsactually read the changed paths — which is howcheck:shell-escape-residueandcheck:doc-linksgot onto the list; neither was named up front, and both readAGENTS.mddirectly.check:governed-queue-guardOK check-governed-queue-guard self-test: 185 cases passcheck:control-bytes✅ check-control-bytes: OK (scanned 7617 tracked text file(s); skipped 85 binary).check:skills-paths✅ check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).check:shell-escape-residue✅ check-shell-escape-residue: OK (5/5 root(s) resolved -- AGENTS.md: 1 file(s), 15 fence(s); …)check:doc-linksLinks are valid across 17 scan roots.check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0check:changeset-claimscheck-changeset-presence✅ No source or published contract of a released package changed in this range, so no changeset is owed.130 passed·120 passed·50 passed·38 passed— 338 cases, 0 failedHook wiring was checked as well, since
settings.jsonis 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:
filesglobs 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.--format json: eslint run over the three changed files reports 3 files, 0 errors.parserOptions.projectorprojectService— so this diff cannot move the verdict on any file it does not touch. Independently:git diff --name-onlyover this branch returns 0 files with a.ts/.tsx/.js/.mjs/.cjsextension. The same reading coverstype-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 lintandpnpm type-checkfarms 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.mjsstill prints the old landing shape. Its refusal text readsPark 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--testoutput at the top of this body. After this pull request,AGENTS.mdstates 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.check-changeset-presencesays 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, anAGENTS.md-only change — carries exactly that shape. ⛔ Noskip-changesetlabel is applied: in this repo that label is read by nothing and exempts nothing.AGENTS.md, verified above. Separately noted, not acted on: its hunk adds a Coding Standard numbered#10, and a commandment#10has since landed onorigin/mainfrom another card, so that branch has two#10s to reconcile when it mergesmain. 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-②: noSession, as prose so it survives a body rewrite: this branch was produced in
session_01DAcomhvR9kKizeYgg89Vo8.Generated by Claude Code