From ec58c6655e251a3f092b97a8bcf166904afc51de Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 00:07:27 +0000 Subject: [PATCH 1/2] docs(settings): deny the 14 MCP GitHub content-writing tools (objectui#9418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 --- .claude/settings.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index 655b17c709..dba825f150 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,6 +11,22 @@ "mcp__Claude_Preview__preview_snapshot", "mcp__Claude_Preview__preview_console_logs", "mcp__Claude_Preview__preview_network" + ], + "deny": [ + "mcp__github__issue_write", + "mcp__github__create_pull_request", + "mcp__github__add_issue_comment", + "mcp__github__add_comment_to_pending_review", + "mcp__github__add_reply_to_pull_request_comment", + "mcp__github__pull_request_review_write", + "mcp__github__push_files", + "mcp__github__create_or_update_file", + "mcp__github__delete_file", + "mcp__github__create_branch", + "mcp__github__sub_issue_write", + "mcp__github__merge_pull_request", + "mcp__github__create_repository", + "mcp__github__fork_repository" ] }, "hooks": { From 660ab2ce32fb5af8afd05cc30c9235a7a8e855c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 00:10:58 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(agents):=20=C2=A79=20states=20ruling?= =?UTF-8?q?=20C=20=E2=80=94=20draft=20until=20an=20authorized=20approval,?= =?UTF-8?q?=20then=20the=20seat=20lands=20(objectui#9418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 --- .../issue-9418-write-identity-locks-port.md | 25 +++++++++++++++++++ AGENTS.md | 10 ++++---- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .changeset/issue-9418-write-identity-locks-port.md diff --git a/.changeset/issue-9418-write-identity-locks-port.md b/.changeset/issue-9418-write-identity-locks-port.md new file mode 100644 index 0000000000..9c6c362d13 --- /dev/null +++ b/.changeset/issue-9418-write-identity-locks-port.md @@ -0,0 +1,25 @@ +--- +--- + +`.claude/settings.json` and `AGENTS.md` only — no released package source and no published +contract field is touched, so this declares **no release** with an empty frontmatter rather +than a bump. `node scripts/check-changeset-presence.mjs` agrees on its own terms ("no changeset +is owed"); the declaration is written anyway because the rule asks for a declaration once, not +for a release. + +objectui#9418 ports the two write-identity locks objectstack landed in objectstack#18072: + +1. `permissions.deny` names the 14 content-writing MCP GitHub tools, so every seat and dev + write leaves through the REST proxy authored `claude[bot]` instead of a person's linked + GitHub account. A suspended user account hides everything it authored — that is what this + closes, and it is measured, not hypothetical. The four state-shaped tools + (`update_pull_request`, `enable_pr_auto_merge`, `actions_run_trigger`, + `resolve_review_thread`) stay allowed: they author no content and the landing path needs them. + +2. §9's governed-surface paragraph stated the landing action as a human merge. Under the + maintainer's ruling C a governed hit stays draft until an authorized APPROVED review exists + (`GOVERNED_APPROVERS`: os-zhuang / hotlong, latest-decisive, not dismissed), and the claiming + seat then lands it. The hard rule is restated in objectstack `lanes/ui.md`'s own words — a + rules-layer paragraph is not paraphrased — and the four other lines that named a human merge + get the minimal word change so the paragraph does not contradict itself. The fifth + prohibition (a seat never approves) and the verbatim maintainer quotes are untouched. diff --git a/AGENTS.md b/AGENTS.md index 8316845027..30f47ec4b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -315,7 +315,7 @@ AGENTS.md 的「只跑受影响的包」指的是**用上面的路径过滤缩 ``` 实测是从 REST 端点发起的;405 正文那句 `Changes must be made through the merge queue` 拒绝的是**「直接合并」这个动作**本身,不是某个客户端,所以旧文教的 `gh pr merge --squash --delete-branch`(不带 `--auto`)这条收尾路径同样不成立(`gh` 具体报什么文案随版本变,**别按文案去猜**,认准下面的入队路径)。**撞上这个 405 不是你权限不够** —— 别去试更强的手段,也别以为要等人工审批。 -- **CI 全绿即自行合并,不必等维护者确认**(授权语义没变,变的只是动作;⛔ **例外:diff 命中受管面的 PR 不适用本条** —— 见下方「受管面」,那类 PR 停在 draft 等人类合并)—— 修改完成后**只提交你任务改动的文件**(逐路径 `git add `,绝不 `git add -A` 扫入无关 diff),开 **draft** PR;等远端 CI 全绿后: +- **CI 全绿即自行合并,不必等维护者确认**(授权语义没变,变的只是动作;⛔ **例外:diff 命中受管面的 PR 不适用本条** —— 见下方「受管面」,那类 PR 停在 draft 等一条获授权的批准,再由认领席落地)—— 修改完成后**只提交你任务改动的文件**(逐路径 `git add `,绝不 `git add -A` 扫入无关 diff),开 **draft** PR;等远端 CI 全绿后: ```bash gh pr ready # 退出 draft @@ -480,7 +480,7 @@ ls /* | wc -l # 两个数不等 ⇒ 工 ⇒ ⭐ **回读发现正文「变短」时,先看渲染后的页面,再决定要不要修。** 一次不必要的重写会毁掉一张本 来正确的卡 —— 而在受管面上,那是不可恢复的。 -### ⛔ 受管面(governed surface):agent 起草,人类合并 +### ⛔ 受管面(governed surface):agent 起草,获授权批准后由认领席落地 维护者裁决(2026-08-18),**原文照录、不翻译** —— 提问明确点名了本仓: @@ -499,13 +499,13 @@ ls /* | wc -l # 两个数不等 ⇒ 工 ⚠️ **两棵 skills 树都受管 —— 别把它们和 skill 的安装位置弄混:** - `.claude/skills/**` —— 内部 agent 工具,在 `.claude/**` 之内,**受管**。 -- `skills/**`(仓根,发布给使用者的那棵,如 `skills/objectui/`)—— **同样受管**,就是 `GOVERNED_SURFACES` 里的 `skills-catalog` 一项:只改 `skills/**` 的 PR 也**停在 draft 等人类合并**,⛔ 不翻 ready、不入队。`.agents/skills/` 是 skill 的**安装位置**(内容由 `skills-lock.json` 还原,第三方的那些被 gitignore),不是规程文本,**不受管**。 +- `skills/**`(仓根,发布给使用者的那棵,如 `skills/objectui/`)—— **同样受管**,就是 `GOVERNED_SURFACES` 里的 `skills-catalog` 一项:只改 `skills/**` 的 PR 也**停在 draft 等一条获授权的批准**,⛔ 批准前不翻 ready、不入队。`.agents/skills/` 是 skill 的**安装位置**(内容由 `skills-lock.json` 还原,第三方的那些被 gitignore),不是规程文本,**不受管**。 两棵树名字像、内容都叫 skill,本段曾按「路径是不是以 `.claude/` 开头」把仓根那棵判成**不受管**、并要求照普通代码 PR 自行入队 —— **那是错的**,而且错在会被机械拒绝的方向上:`merge_group` 腿照样拒,照着那句话做的席位要赔上一整轮队列构建。判据以 `scripts/check-governed-queue-guard.mjs` 的 `GOVERNED_SURFACES` 为准,拿不准就直接问它:`node scripts/check-governed-queue-guard.mjs --test `。 **硬规则 —— PR 的 diff 命中受管面时:** -⛔ 绝不 `gh pr ready`(不退出 draft)、⛔ 绝不加入合并队列、⛔ 绝不 `gh pr merge --auto` / `enable_pr_auto_merge`、⛔ 绝不自己合并。这类 PR **停在 draft,等人类合并**。**人类的那次合并动作本身就是审核记录** —— 不需要额外的逐 PR 批准点击,也别去等一个不存在的 approval。 +⇒ **命中即停 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 后不管后续改动都由席位落地:」)。 ⛔ **第五条禁令 —— 绝不自己去留下那条 approval。** 上面四条管的是**落地**,这一条管的是**批准**:`scripts/check-governed-queue-guard.mjs` 的文件头部把它写成规范条款,它 `cleared` 分支的判定文本也印着同一句。此处**逐字照录、不译**(两处措辞不得漂移): @@ -514,7 +514,7 @@ ls /* | wc -l # 两个数不等 ⇒ 工 sha pin **退休**之后这条**更重、不是更轻**(维护者 2026-09-04 裁,#7606 执行、#7616 把新判据写进下面那段):一条获授权的 APPROVED review 现在清掉同一 PR 其后**每一次** push,于是在一个 agent 操作的 approver 账号与一次它自己放行的受管落地之间,**只剩这条规范禁令**。 - **判据是 PR 的文件清单,不是 PR 的标题或描述。** 命中与否只看路径。 -- **混合 diff:一条命中即整个 PR 分叉,没有比例判断。** 99 个普通文件 + 1 个受管文件 = 整个 PR 等人类合并。其余部分急着落地,就把受管文件**拆成单独的 PR**,别用「占比很小」给自己开口子。 +- **混合 diff:一条命中即整个 PR 分叉,没有比例判断。** 99 个普通文件 + 1 个受管文件 = 整个 PR 等那条获授权的批准。其余部分急着落地,就把受管文件**拆成单独的 PR**,别用「占比很小」给自己开口子。 - **起草不受限。** 写、推分支、开 PR、按 review 修改,每个席位照做不误;被保留的只有**落地**这一个动作。 - **CI 全绿、已 review 都不构成例外。** 这类文件是后续每一次 dispatch 读的操作规程,绿灯说明不了它该不该成为规程。 - **发现自己已经挂上了怎么办**:把 PR 转回 **draft** 是唯一能可靠退出合并队列的动作 —— 只调 `disable_pr_auto_merge` 会摘掉 auto-merge 但**不取消队列成员资格**,两个都要做。⚠️ 只回收**你自己**挂上的:本仓多 agent 共用同一 GitHub 身份,不是你设置的状态就属于别的 actor —— 去问、去报告,别替他回退。