docs(mcp): name the Account door for Connect an Agent, not Setup alone - #18142
Conversation
#17646 delivered #16746's ruling as a `navigationContributions` entry in the `account` app and deliberately left Setup gated, so a non-admin reaches the Connect-an-Agent page — but at none of the paths the shipped texts named. - `packages/mcp/src/plugin.ts` — the stdio refusal message now names both doors (Account → Developer for any signed-in user; Setup for admins). - `packages/mcp/README.md` — same, in the `OS_MCP_STDIO_API_KEY` paragraph. - `content/docs/ai/connect-mcp.mdx` — the "Headless: API keys" section now gives both doors with their console URLs, and moves the revoke location to `Account → Developer → API Keys` for the user's own keys, noting the tenant-wide Setup list needs `manage_platform_settings`. The `OS_MCP_SERVER_ENABLED=false` callout no longer calls it a Setup page. Paths, labels and permissions read off the tree, not invented: the account entry at `packages/mcp/src/connect-ui.ts`, the group at `packages/platform-objects/src/apps/account.app.ts`, the package id at `packages/apps/account/src/index.ts`, and the route resolution in objectui's `packages/app-shell/src/utils/appRoute.ts`. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 86080a316bd7841058bde08fb88e9db43f31e574 && git checkout 86080a316bd7841058bde08fb88e9db43f31e574
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a90a9f26794e5a2c34c1eded83ba0e25087e4433 680f338de454b29dfe0285efe502127fa205fe7c && git checkout -B drift-repro a90a9f26794e5a2c34c1eded83ba0e25087e4433 && git merge --no-ff 680f338de454b29dfe0285efe502127fa205fe7c
node scripts/docs-audit/affected-docs.mjs --json a90a9f26794e5a2c34c1eded83ba0e25087e4433
|
复核通过 —— 已 undraft 并武装(
|
| 探针 | 读数 |
|---|---|
APP_NAME |
不存在;:160 起是 const APPS = [{setup},{account}],:166 APP_NAMES = APPS.map(...) |
| 该门禁的 roster | :511 明写 { source: '@objectstack/mcp', apps: ['setup','account'], idsByApp: { setup: [], account: ['nav_connect_agent'] } } |
⇒ 那道门禁看得见 account 侧入口(PR #17972 / #17891 拓宽的)。本席这条前提是陈旧的,dev 报回来是对的。
⭐ 但卡的结论仍然成立,理由换了一条,这点 dev 说得准:那道门禁判的是locale bundle 的标签,⛔ 从不判英文散文 —— 所以这三处 prose 断言仍然没有任何机器在看。⇒ 卡该做,做法不变;变的只是"为什么没人看见"。
事实逐条重测(⛔ 不采信报告)
新写进文档的每一条事实,本席都在树里落到了锚点:
| 文档新写的话 | 树里的出处 |
|---|---|
Account 应用直链 com.objectstack.account |
packages/apps/account/src/index.ts ACCOUNT_APP_PACKAGE_ID = 'com.objectstack.account' |
| Account → Developer → API Keys(自己的钥匙) | platform-objects/src/apps/account.app.ts — grp_account_developer(label Developer)下 nav_account_api_keys(label API Keys,viewName: 'mine')⇒「自己的」这半是准的 |
Setup → Access Control → API Keys 需 manage_platform_settings |
setup-nav.contributions.ts:102 nav_api_keys … requiredPermissions: ['manage_platform_settings'];setup.app.ts:76 label: 'Access Control' |
Setup 门需 setup.access |
该权限在树里真实存在 |
nav_connect_agent 落在 grp_account_developer |
packages/mcp/src/connect-ui.ts(并有 connect-agent-account-nav.test.ts 钉着) |
空对照一律读 0;packages/account / packages/setup,实为 packages/apps/**),读到的 0 ⛔ 不是读数 —— 换对路径并加发火对照后才得出上表。一条错路径的零不是一次阅读,本班又一次。
改动本身
三处都名了两道门(任意登录用户走 Account,管理员走 Setup),⭐ 而 Setup 那道原样保留 —— 这正是卡里要求的"不要用一条路替换另一条":#17646 没有动 Setup,管理员的路径依然有效。
connect-mcp.mdx 是整段重读过的,侧栏链接与吊销位置两处同病都改了;并顺手修了同文件同病的 OS_MCP_SERVER_ENABLED 提示框(同一缺陷类、同一文件,⛔ 不算扩面)。
文件面 = 本席声明的三处 + changeset,⛔ 无越界;changeset 用通行的 <issue>-<slug> 拼法。
核过的其余项
check-clause2-carriers --pair 18142→ 两载体一致、diff 无放宽征兆 ✅- CI:40 项,RED: none(23 success / 4 skipped / 13 in_progress);未挂
needs:contract-review✅ - ⛔ 不触治理面 ✅
附带产出与交接
- dev 另立 Four more shipped docs pages still send a non-admin to "Setup → Connect an Agent", the one app that 403s for them #18143:另有四处已发布文档带同样缺陷,在本卡声明的文件面之外(
api/index.mdx与deployment/environment-variables.mdx是直接的铸钥指引,ai/agents.mdx与getting-started/build-with-claude-code.mdx是描述性的)。⛔ 本席停席前不派它。 - dev 明确未立的一项:把 advisory 的 docs-drift 检查扩成「散文里点名的 console 路径必须解析到已注册的 app + page」的真门禁 —— 简报要求"只报不建",dev 照办了。⇒ 这是一条立卡候选,交给接手席位或 Four more shipped docs pages still send a non-admin to "Setup → Connect an Agent", the one app that 403s for them #18143 的接手人。
⚠️ 跨车道声明随本席停席失效:packages/mcp/**属domain:cli行,本席曾点名声明并知会 [PM seat] domain:cli — 🟢 os-warren · session_01RWZbGvPFcRKvUqASZtunCU #6024(评论5659612310)。本 PR 已武装、即将落地,该声明对已落的这一笔有效;但 ⛔ 后续任何在packages/mcp/**的动作需要接手席位重新声明并重新知会,⛔ 不得默认继承。
Generated by Claude Code
…O_API_KEY row (objectstack-ai#18959) Fixes objectstack-ai#18143 Clause-②: no ## The remainder — one line, one file This card named **four** sites. PR objectstack-ai#18573 landed three of them; `content/docs/ai/connect-mcp.mdx` belongs to objectstack-ai#17648. What was left is the fourth: the `OS_MCP_STDIO_API_KEY` row in `content/docs/deployment/environment-variables.mdx`, located **by content**, not by the line number the card quotes. | | the cell | |:--|:--| | before | … Mint one from **Setup → Connect an Agent** (or `POST /api/v1/keys`). … | | after | … Mint one from the **Connect an Agent** page — **Account → Developer** for any signed-in user, **Setup → Connect an Agent** for platform admins — or `POST /api/v1/keys`. … | One line in, one line out. It is a table cell in a long Markdown table, so the two-door sentence is compressed to fit: pipe count unchanged (5), row count unchanged (133 `OS_` rows), still a single line. ## Why the old cell was wrong `SETUP_APP` declares `requiredPermissions: ['setup.access']`, and a permissionless principal gets `403 PERMISSION_DENIED` on `/api/v1/meta/apps/setup`. A **direct minting instruction** naming only the Setup door therefore tells a non-admin to take a path they cannot take. Ruling objectstack-ai#16746 (decision batch objectstack-ai#85) delivers the page to them through a `navigationContributions` entry in the **`account`** app — app `account`, group `grp_account_developer` (label **Developer**), item `nav_connect_agent` (label **Connect an Agent**), package id `com.objectstack.account`. The Setup entry **stays** for admins, deliberately. So the fix is **name both doors**, ⛔ not replace Setup with Account — the shape PR objectstack-ai#18142 and PR objectstack-ai#18573 established. The wording here is copied from the two sibling pages rather than invented as a fourth spelling: - `content/docs/api/index.mdx:68-69` — "…from the **Connect an Agent** page in the Console — **Account → Developer** for any signed-in user, **Setup → Connect an Agent** for platform admins." - `content/docs/getting-started/build-with-claude-code.mdx:435-436` — "…lives on the **Connect an Agent** page: **Account → Developer** for any signed-in user, **Setup → Connect an Agent** for platform admins." ## Post-condition probe — written BEFORE the edit, and deliberately NOT "Setup goes to 0" An earlier round's first probe was "`Setup → Connect an Agent` must go to 0 in this file". That probe is **wrong for this card**: the correct end state keeps the Setup door named, so it would read a correct landing as a half-done one. The post-conditions here are about the **Account door appearing alongside**. Every count is taken on a **whitespace-flattened** file, so wrapped prose cannot give a false zero, and every zero is paired with a control from the same population that must hit. | # | reading (flattened) | before | after | post-condition | |:--|:--|--:|--:|:--| | A | this file, `Account → Developer` | 0 | **1** | ≥ 1 — the Account door appears | | B | this file, `Setup → Connect an Agent` | 1 | **1** | ≥ 1 — Setup **stays** named, for admins | | C | CONTROL, this file, `Connect an Agent` unprefixed | 1 | 2 | nonzero both sides — the reader has a pulse | | D | table integrity: `OS_` rows / pipes in the row / lines for that key | 133 / 5 / 1 | 133 / 5 / 1 | unchanged, single line | | E | CORPUS CONTROL over `content/docs/**/*.mdx` (404 files), `Connect an Agent` unprefixed | 10 | 11 | nonzero — the corpus reader has a pulse | Corpus-level close-out: the Setup door is still named in exactly **4** files (unchanged by design), and **every one of the 4 now also names the Account door** — carriers naming the Setup door but not the Account door: **0**. | carrier | `Setup → Connect an Agent` | `Account → Developer` | |:--|--:|--:| | `content/docs/ai/connect-mcp.mdx` | 1 | 1 | | `content/docs/api/index.mdx` | 1 | 1 | | `content/docs/deployment/environment-variables.mdx` | 1 | 1 | | `content/docs/getting-started/build-with-claude-code.mdx` | 1 | 1 | ## Serial constraint — re-measured at hunk level, and it does not bite PR objectstack-ai#18420 (draft, untouched since 2026-09-17T16:16Z) is the only open PR touching this file. Read from its diff: its **only** hunk in this file is `@@ -87,7 +87,7 @@`, the `OS_AUTOMATION_SCHEDULED_WORK_ENABLED` row. This PR changes the row at `:260`. **173 lines apart**, far outside git's three-line context ⇒ no textual conflict. Nothing in objectstack-ai#18420 was touched or coordinated. ## Verification Gate families derived in this worktree from the real change set, not from a hand-written list: `node scripts/pm/dispatch-gates.mjs --commands` (change set: 1 path vs merge base `46559f61c`). - **39 derived families, 39 run, all `exit 0`.** Reconciled with exit codes recorded: `dispatch-gates --repo objectstack-ai/objectstack --ran` ⇒ "39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3)". - Four of them first returned `PREREQUISITE NOT MET` (`exit 3` ×3, plus `check:skill-examples` exit 1 on an unbuilt `client-react` dist) — **not findings**. After `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react --filter=@objectstack/client` (exit 0) all four re-ran at `exit 0`: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`. - `pnpm --filter @objectstack/spec build` ran first (exit 0), so `check:docs` read a current tree. - That derivation is **not** a complete account of CI — the artifact-roster, wide-population, pending-changeset and path-scheduled families sit outside it, as the tool says of itself. - Control characters: `grep -naP` over the edited file finds none (exit 1), with a planted positive control proving the reader fires (exit 0, hit). `pnpm check:nul-bytes` exit 0. ### `pnpm lint` — a **proven narrowing**, not a skipped run The repo-wide scan is CI's run. Three pieces of evidence that narrowing excluded nothing: 1. **Population, read from eslint's own config:** every `files:` glob in `eslint.config.mjs` enumerates code extensions (`ts,tsx,mts,cts,js,jsx,mjs,cjs`); the string `mdx` occurs **0** times in that config. `.mdx` is not in the linted population at all. 2. **File count, read from `--format json`:** eslint over the changed file returns **0 results**; the positive control (`scripts/check-nul-bytes.mjs`) returns **1 result** — the reader resolves files and reports. 3. **Invariance for untouched files:** the config enables no type-aware linting for any file (its own header: "this repo runs one `eslint.config.mjs`, which never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file"), so this diff cannot move any untouched file's verdict. ### Changeset: `skip-changeset`, measured Nothing published moves. - 83 tracked manifests; **70** declare `files[]` (the control: the reader resolves `files[]` arrays — e.g. `@objectstack/spec` ⇒ `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`). Entries reaching `content/docs/**`: **0**. - Symbol grep over the **2138** files those `files[]` entries actually resolve to: `Mint one from` ⇒ **0**, `Account → Developer` ⇒ **0**; positive control `objectstack` ⇒ **1971** files, so the reader reaches published bytes. - The only consumer of `content/docs/`, `@objectstack/docs` (`apps/docs`), is `private: true` and declares no `files[]`. - The one published manifest whose text mentions `content/docs` (`@objectstack/plugin-webhooks`) does so in its `description` prose about a different page; its `files[]` is `dist`, `README.md`, `CHANGELOG.md`. ## Acceptance notes Out of scope, noted and **not** filed: - The card's four deliberately excluded carriers (`docs/adr/0101-…:104`, `docs/qa/platform-checklist/areas/ai.json:206`, two `.changeset/*.md`) are dated records, left untouched. - This same file carries `Setup → Settings` and `Setup → Authentication`, and the corpus carries 27 other `Setup → X` phrases (Access Control, People, SSO Providers, Datasources, Approvals …). Those name genuinely admin-only surfaces addressed to admins — the Connect-an-Agent defect exists precisely because that one page is **also** delivered to non-admins through the `account` app, which is not true of the others. No defect, and the successor question has an answer: **successor: none** — no PR or reader is routed to them by this change. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
Closes #17648
Clause-②: no#17646 delivered #16746's ruling by adding a second
navigationContributionsentry into the
accountapp — deliberately not by ungating Setup, whichwas measured to expose 14+ unrelated Setup surfaces. So a non-admin can now
reach the Connect-an-Agent page, but at none of the paths the three shipped
texts named. This edits the prose; nothing else moves.
The Account path, measured (not invented)
accountpackages/mcp/src/connect-ui.ts(the second contribution)grp_account_developer, label Developerpackages/platform-objects/src/apps/account.app.ts; label inapps/translations/en.tsnav_connect_agent, label Connect an Agentconnect-ui.ts; label inen.ts(all four locales, per #17759)connect_agentCONNECT_AGENT_PAGEinconnect-ui.tscom.objectstack.accountpackages/apps/account/src/index.ts, wired atpackages/cli/src/commands/serve.ts/apps/:appName/page/:pageNamepackages/app-shell/src/console/AppContent.tsx_packageIdfirst, appnameas aliaspackages/app-shell/src/utils/appRoute.ts—matchAppBySegmentpackages/app-shell/src/layout/AppHeader.tsx⇒
/_console/apps/com.objectstack.account/page/connect_agent, symmetric with theSetup URL the page already carried.
packages/apps/account/src/index.tsstatesthe pair in as many words: "
/apps/(packageId)(alias/apps/account) resolvesto exactly this app".
Permissions, also measured:
SETUP_APPdeclaresrequiredPermissions: ['setup.access'](setup.app.ts:47), and Setup's API-keys entry additionallyrequires
manage_platform_settings(setup-nav.contributions.ts:102), whileACCOUNT_APPdeclares none. The Account app's own API Keys entry is theminelist view filtereduser_id == {current_user_id}with therevoke_api_keyrow action — so the revoke fact survives the move rather thanbeing dropped.
The three sites
1.
packages/mcp/src/plugin.ts— the stdio refusal message (a runtimestring, read exactly when the user is stuck). Found at :384, not the card's
:372— the reading had rotted; located by content.mint an API key (Setup → Connect an Agent, or POST /api/v1/keys)mint an API key on the Connect an Agent page (Account → Developer for any signed-in user; Setup → Connect an Agent for admins), or POST /api/v1/keys2.
packages/mcp/README.md:92(line unmoved) — same substitution, in theOS_MCP_STDIO_API_KEYparagraph, with the README's existing bold convention.3.
content/docs/ai/connect-mcp.mdx— the "Headless: API keys" section(97–104, unmoved) rewritten as one page, two doors, each with its console
URL and its permission; the revoke sentence now sends a user to Account →
Developer → API Keys and labels the tenant-wide Setup → Access Control → API
Keys list with the permission it needs.
Bounded in-place fix in the same file and defect class: the
OS_MCP_SERVER_ENABLED=falsecallout at :14 also called it "the Setup →Connect an Agent page". It now says "the Connect an Agent page … along
with both its Setup and Account navigation entries", which is what #17646's own
changeset measured (an opted-out deployment gets no page and neither entry).
Reverse-read, both directions
Account path". Reproduced on
origin/mainbefore editing —Account app//_console/apps/account/grp_account_developerovercontent/docs/= 3hits (an authorization note, an objectui action target, a v17-0 release page),
all unrelated; firing control on the same expression = 5. That count is the
card's, not shipped prose, and is history once this lands.
followable by a permissionless principal, and the refusal message is actionable
for an operator who is not a platform admin.
control — tests referencing
OS_MCP_STDIO_API_KEY= 5 files). No pin testreads this page's prose (control —
scripts/docs-audit/handwritten-docs.jsonlists the file, so the path is right).
Verification
Repo-wide, not narrowed:
pnpm lint(eslint . --no-inline-config) exit 0in 74s at
680f338de4.pnpm --filter @objectstack/mcp build && typecheck && test— 31 files, 333tests passed, under
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0).Dependency closure
pnpm --filter '@objectstack/mcp^...' build—VERDICT command-exit 0.Gate families derived from the real change set with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackand reconciled with
--ran: 83 derived, 81 run green, 2 NOT MEASURED, 0unrun. The two are
check:dual-build-cjs-loadsandcheck:lean-entry-closure,both exit 3 / PREREQUISITE NOT MET — they read built output across ~77
packages this worktree has not built. ⛔ Not read as passes; declared to CI's
Build Core job.
check:skill-examplesalso refused a prerequisite first; I built@objectstack/client+@objectstack/client-reactand re-ran it to a realverdict (258 prose examples type-check across 3 surfaces).
Control-character self-scan over the four touched files: clean, with a firing
control (
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]').No ablation: the change is prose and one string literal — there is no guard to
delete and no assertion whose failure mode could be proven by mutation.
验收备注
A brief premise that is now FALSE, and worth the seat's attention. The
dispatch and the card both state that
packages/cli/scripts/check-app-nav-i18n.mjs"scopes itself toAPP_NAME = 'setup'(:109) and skips every other contribution target (:581), so nothingjudges the account-side entry". That was true when #17648 was filed and is
superseded: PR #17972 (#17891) widened it to a declared population,
APPS = [{ name: 'setup' }, { name: 'account' }]at :161-164, withAPP_NAMESat:166 and the self-test invariants at :575/:580. The file's own header now names
nav_connect_agent/grp_account_developer/connect-ui.tsexplicitly.The card's conclusion still holds, for a different reason than it gave: that
gate judges locale-bundle labels, never English prose in docs, a README, or a
thrown
Error. Nothing machine-checks these three claims, so the prose edit wasstill the only remedy.
Out of scope, filed separately as #18143 — four more shipped pages carry the identical
defect but lie outside this card's declared file surface:
content/docs/ai/agents.mdx:55,content/docs/api/index.mdx:68,content/docs/getting-started/build-with-claude-code.mdx:435,content/docs/deployment/environment-variables.mdx:259. The last two are directmint instructions, the same shape as the three fixed here.
Noted, not filed:
docs/adr/0101-mcp-stdio-principal-admission.md:104,docs/qa/platform-checklist/areas/ai.json:206and two.changeset/files alsoname "Setup → Connect an Agent". All four are dated records — a ruling, a test
checklist and shipped release history — so ⛔ not edited and ⛔ not filed.
Card candidate deliberately NOT built here: a cheap way to make these claims
machine-checkable would be to extend the docs-drift check from advisory to a real
gate over "console path named in prose resolves to a registered app + page".
Out of scope for a p1 prose fix; reported rather than built, per the brief.
🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code