fix(mcp): contribute Connect an Agent into the Account app nav so a non-admin can mint their own key - #17646
Conversation
`POST /api/v1/keys` mints a `sys_api_key` bound to the caller and the page says the key "acts as you", but the only nav entry sat in Setup behind `requiredPermissions: ['setup.access']` — so every non-admin following the two-step guide stopped at step 1 while the endpoint behind the button accepted them all along. Adds a second `navigationContributions` entry in the same bundle, targeting the `account` app's `grp_account_developer` group beside the `nav_account_api_keys` entry already shipping there. The Setup entry stays for admins. Backend, authorization and the published "acts as you" promise do not move, and no gate of any kind is added or removed: a contribution registers exactly when the page registers. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
…Setup guard Pins the half this package owns: the contribution is aimed at the ungated `account` app / `grp_account_developer` group, its item carries nothing the server-side nav filter could strip for a permissionless caller, the Setup entry is byte-unchanged, and — the load-bearing one — this bundle declares no permission key and no `apps` collection, so it cannot reach the card by widening Setup instead. Both contributions are parsed against the real `NavigationContributionSchema`, which is what makes the shared item id an accepted fact rather than an unenforced one. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
…an-Agent entry Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
…nnect-agent-account-nav
📓 Docs Drift CheckThis PR changes 1 package(s): 13 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 743a70a30577f20c3938598c4805bd0e0c9cca1f && git checkout 743a70a30577f20c3938598c4805bd0e0c9cca1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1f0b5659e430717645877f01c4128be567731e35 32f62417bdac521233be5445f9e657a607146558 && git checkout -B drift-repro 1f0b5659e430717645877f01c4128be567731e35 && git merge --no-ff 32f62417bdac521233be5445f9e657a607146558
node scripts/docs-audit/affected-docs.mjs --json 1f0b5659e430717645877f01c4128be567731e35
|
#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>
…onnect_agent` in all four locales (objectstack-ai#17887) Fixes objectstack-ai#17759 Clause-②: yes ## What was wrong `@objectstack/mcp` contributes the Connect an Agent page into **two** apps — `setup` (admins) and, since PR objectstack-ai#17646, `account` → Developer (every authenticated user). Translation bundles are keyed `apps.APP.navigation.ID`, one namespace per app, and only the `setup` key existed. So `apps.setup.navigation.nav_connect_agent` never answered for the Account door, and one destination rendered two different strings for the same signed-in user. The population that got the English literal is exactly the non-admin on a non-English locale: Account is the only one of the two doors they can open. ## What changed **Four locale entries** in the hand-authored bundles, mirroring the Setup twin verbatim, and **three provenance rows** in the hand-maintained `LOCALE.source-hashes.ts` tables. There is no `en.source-hashes.ts` and none is invented: `en` is the SOURCE, not a copy of one, so it carries no provenance row. Four entries, three rows. The generated companions (`LOCALE.source-hashes.generated.ts`) are **not** touched. `apps` is a `HAND_AUTHORED_SECTIONS` section, the extractor never writes the hand table, and a digest written into the generated one is overwritten on the next extract. Measured on this branch: all three generated tables carry **0** `apps.*` rows, before and after. `zh-CN.source-hashes.ts`'s header says a brand-new translation needs no entry (a path with no recorded hash is legacy-trusted). The three rows are recorded anyway because the same header says recording one now is strictly better — it will catch the NEXT source edit. ## Measurement — all four entries, all three rows Resolved off the bundles, not off a grep of the file text, with a fabricated-id negative control through the identical lookup path: ``` apps.account.navigation.nav_connect_agent.label en "Connect an Agent" NEG apps.account.navigation.nav_connect_agent_xyzzy -> undefined zh-CN "连接智能体" NEG -> undefined ja-JP "エージェントを接続" NEG -> undefined es-ES "Conectar un agente" NEG -> undefined recorded digest, apps.account.navigation.nav_connect_agent.label zh-CN eeb174613510e87d NEG apps.account.navigation.nav_connect_agent_xyzzy.label -> undefined ja-JP eeb174613510e87d NEG -> undefined es-ES eeb174613510e87d NEG -> undefined ``` The card's own grep shape, re-taken on this branch in `packages/platform-objects/src/apps/translations/`: ``` apps.account.navigation.nav_connect_agent -> 0 before / 3 after (the three digest rows) CONTROL nav_account_api_keys -> 7 (fires ⇒ the zero was a reading, not a dead grep) ``` The control is the card's: a key that genuinely lives under the account app, in the same directory, through the same grep, at the same citation form — 3 digest rows + 4 locale entries = 7, which is the shape this change now also has. ## How each digest was computed `collectSourceHashes(en)['apps.account.navigation.nav_connect_agent.label']`, exported from `source-hash.ts` — the procedure that file's own header prescribes. ⛔ Not hand-written. ``` collectSourceHashes(en)[apps.account.navigation.nav_connect_agent.label] = eeb174613510e87d collectSourceHashes(en)[apps.setup.navigation.nav_connect_agent.label] = eeb174613510e87d (committed row agrees) CONTROL collectSourceHashes(en)[...nav_account_api_keys.label] = 8cda9851248b0490 (committed row agrees) NEGATIVE hashSource('Connect an Agentx') = 9ca584fc78a6efb6 ``` The two committed rows the function reproduces byte-for-byte are what makes this the function that produced the existing tables; the one-character negative shows the function is not a constant. **The digest values are proven correct, with a firing control.** `findStaleLeaves(bundle, en, committedTable)` reports our path stale in **none** of the three locales (0 stale leaves in each bundle); the identical call with our digest corrupted by one hex character reports it stale in **all three**, naming `recorded eeb174613510e87e` vs `current eeb174613510e87d`. A wrong digest would have made `withSourceFallback` serve the English source in place of the translation — re-creating exactly the defect this card fixes — so this is the control that matters here. ## Clause-② — the measurement the seat delegated Declared `yes` on the mechanical floor (a new key on a published payload). ⛔ A literal grep of the entry for the module PATH reads 0 and misleads, because the chain is a star chain — that method is not used here. Taken instead on the **built** entry, `packages/platform-objects/dist/index.d.ts`, which is what `exports['.'].types` names and what `files: ["dist"]` ships: ``` en -> 1 zhCN -> 1 jaJP -> 1 esES -> 1 SetupAppTranslations -> 1 NEGATIVE CONTROL, same grep, same file: zhTW -> 0 koKR -> 0 ptBRxyzzy -> 0 SetupAppTranslationsXyzzy -> 0 ``` And the payload itself, imported off the built `dist/index.mjs`: `SetupAppTranslations['zh-CN'].apps.account.navigation.nav_connect_agent.label` reads `"连接智能体"` (and the other three locales likewise), with the fabricated id `undefined` in every one. ⇒ The bundles reach the published surface. The declaration at the top of this body stands, `needs:contract-review` stays on both carriers, and the changeset is graded **`@objectstack/platform-objects: minor`** — an additive key on a published payload, not a `patch`. ## Verification | what | result | |:--|:--| | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | 54 derived commands, all run, **54 run / 0 NOT-MEASURED / 0 UNRUN**, reconciled with `--ran` carrying every exit code | | exit-3 handling | `check:dual-build-cjs-loads`, `check:i18n`, `check:lean-entry-closure` each answered **exit 3 = PREREQUISITE NOT MET** on the fresh worktree. Built first, re-ran, all three **exit 0**. ⛔ No exit 3 is recorded as a pass. | | the i18n family, run in full | `check:i18n` 0 · `check:i18n-coverage` 0 · `check:i18n-walk-parity` 0 · `check:i18n-stale-fill` 0 · `check:app-nav-i18n` 0 | | roster gates whose roster sits under a touched directory | `check-changeset-fixed` 0 · `check:authz-resolver` 0 · `check:error-code-casing` 0 · `check:filter-alias-parity` 0 | | `pnpm --filter @objectstack/platform-objects test` (⛔ unnarrowed, no `--project`) | **40 files / 571 tests passed**, single untiered project | | `pnpm --filter @objectstack/platform-objects typecheck` | 0 | | `pnpm --filter @objectstack/mcp test` (unnarrowed) | **31 files / 333 tests passed** | | `pnpm --filter @objectstack/cli test` (unnarrowed, both tiers) | **247 files / 3292 tests passed** — `vitest list --filesOnly` reports 202 `unit` + 45 `integration` = 247, so both tiers ran and no file was dropped | `check:app-nav-i18n` prints `OK (10 contributor(s), 54 merged setup nav id(s), 4 locale(s))` — unchanged, which is the point: that gate scopes itself to `APP_NAME = 'setup'` and is structurally blind to the account contribution both before and after this change. Every heavy run went through `scripts/pm/os-verify-lock.sh`; every gate exit code was captured by redirect-then-capture, never across a pipe. ## Acceptance notes - **noted, not filed:** extending `packages/cli/scripts/check-app-nav-i18n.mjs` past `APP_NAME = 'setup'` is deliberately not in this PR. The standing repair order puts a check last, and that extension lands in `packages/cli/scripts/**` under `domain:cli`. 承接者: the follow-up card objectstack-ai#17759's Scope section says is worth filing once this one closes. - **noted, not filed:** `app-nav-translation-parity.test.ts` asserts its reverse direction ("no translation outlives its declaration") for `STUDIO_APP` alone, so the key added here is structurally exempt from it — correctly, since the Account entry is contributed at runtime and no static walk can see its declaration. Extending that assertion to `ACCOUNT_APP` would go red on this very key. 承接者: the same follow-up gate card above; it is the same lane and the same question. - **noted, not filed (boundary, class not widened):** nothing asserts a recorded digest's VALUE. `source-hash.test.ts` checks the shape (16 lower-case hex) and that the path exists in `en.ts`, so a mistyped digest ships green and silently serves the English source in that locale. It is not filable: a gate asserting `recorded === hashSource(source)` is precisely the Option C the objectstack-ai#8765 ruling rejected — it would red on every un-re-translated source edit, which is the state the mechanism exists to tolerate. Recorded as a boundary. 承接者: 无. ⛔ No behaviour outside the bundle moves: no nav item, permission, route or page is added, the contribution and the destination already existed and are untouched, and the Setup key is byte-unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
objectstack-ai#18142) Closes objectstack-ai#17648 `Clause-②: no` objectstack-ai#17646 delivered objectstack-ai#16746's ruling by adding a second `navigationContributions` entry into the **`account`** app — deliberately *not* by ungating Setup, which was 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) | fact | value | read from | |:--|:--|:--| | target app | `account` | `packages/mcp/src/connect-ui.ts` (the second contribution) | | group | `grp_account_developer`, label **Developer** | `packages/platform-objects/src/apps/account.app.ts`; label in `apps/translations/en.ts` | | item | `nav_connect_agent`, label **Connect an Agent** | `connect-ui.ts`; label in `en.ts` (all four locales, per objectstack-ai#17759) | | page | `connect_agent` | `CONNECT_AGENT_PAGE` in `connect-ui.ts` | | package id | `com.objectstack.account` | `packages/apps/account/src/index.ts`, wired at `packages/cli/src/commands/serve.ts` | | route shape | `/apps/:appName/page/:pageName` | objectui `packages/app-shell/src/console/AppContent.tsx` | | segment resolution | `_packageId` first, app `name` as alias | objectui `packages/app-shell/src/utils/appRoute.ts` — `matchAppBySegment` | | how a user gets in | avatar menu → **Profile** mounts the Account shell; Developer stays reachable from its sidebar | objectui `packages/app-shell/src/layout/AppHeader.tsx` | ⇒ `/_console/apps/com.objectstack.account/page/connect_agent`, symmetric with the Setup URL the page already carried. `packages/apps/account/src/index.ts` states the pair in as many words: *"`/apps/(packageId)` (alias `/apps/account`) resolves to exactly this app"*. Permissions, also measured: `SETUP_APP` declares `requiredPermissions: ['setup.access']` (`setup.app.ts:47`), and Setup's API-keys entry additionally requires `manage_platform_settings` (`setup-nav.contributions.ts:102`), while `ACCOUNT_APP` declares none. The Account app's own **API Keys** entry is the `mine` list view filtered `user_id == {current_user_id}` with the `revoke_api_key` row action — so the *revoke* fact survives the move rather than being dropped. ## The three sites **1. `packages/mcp/src/plugin.ts`** — the stdio refusal message (a runtime string, read exactly when the user is stuck). Found at **:384**, not the card's `:372` — the reading had rotted; located by content. - before: `mint an API key (Setup → Connect an Agent, or POST /api/v1/keys)` - after: `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/keys` **2. `packages/mcp/README.md:92`** (line unmoved) — same substitution, in the `OS_MCP_STDIO_API_KEY` paragraph, 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=false` callout 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 objectstack-ai#17646's own changeset measured (an opted-out deployment gets no page and neither entry). ## Reverse-read, both directions - **Made false:** objectstack-ai#17648's own measurement *"nothing in the docs names the Account path"*. Reproduced on `origin/main` before editing — `Account app` / `/_console/apps/account` / `grp_account_developer` over `content/docs/` = **3** hits (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. - **Made true:** the mint instruction and the revoke instruction are now followable by a permissionless principal, and the refusal message is actionable for an operator who is not a platform admin. - **Zero results, reported:** no test pins the refusal-message text (0 hits; control — tests referencing `OS_MCP_STDIO_API_KEY` = 5 files). No pin test reads this page's prose (control — `scripts/docs-audit/handwritten-docs.json` lists the file, so the path is right). ## Verification Repo-wide, not narrowed: `pnpm lint` (`eslint . --no-inline-config`) **exit 0** in 74s at `680f338de4`. `pnpm --filter @objectstack/mcp build && typecheck && test` — **31 files, 333 tests 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/objectstack` and reconciled with `--ran`: **83 derived, 81 run green, 2 NOT MEASURED, 0 unrun**. The two are `check:dual-build-cjs-loads` and `check: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-examples` also refused a prerequisite first; I built `@objectstack/client` + `@objectstack/client-react` and re-ran it to a real verdict (**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 to `APP_NAME = 'setup'` (`:109`) and skips every other contribution target (`:581`), so nothing judges the account-side entry"*. That was true when objectstack-ai#17648 was filed and is **superseded**: PR objectstack-ai#17972 (objectstack-ai#17891) widened it to a declared population, `APPS = [{ name: 'setup' }, { name: 'account' }]` at **:161-164**, with `APP_NAMES` at :166 and the self-test invariants at :575/:580. The file's own header now names `nav_connect_agent` / `grp_account_developer` / `connect-ui.ts` explicitly. 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 was still the only remedy. **Out of scope, filed separately as objectstack-ai#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 direct mint 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:206` and two `.changeset/` files also name "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.com/claude-code) https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16746
Clause-②: no
Declared by the dispatching
domain:cliPM seat (sessionsession_01TSf4DV7ziu4V5j73e46b7c), matching the claim comment on #16746. Verified against the DIFF: the payload gains one array ELEMENT of an existing shape, not a new key —NavigationContributionSchemais untouched and both contributions parse against the real contract (pinned in this PR). 0 new error codes. The permission-VISIBILITY aspect is deliberately not clause-②: a runtime permission/security behaviour change sits on the manual floor, and that floor was discharged by the maintainer ruling of 2026-09-08 (decision batch #85, option A, verbatim 「其他同意」), not by this declaration.What changed
CONNECT_AGENT_UI_BUNDLE(packages/mcp/src/connect-ui.ts) now carries a secondnavigationContributionsentry, the near-twin of the existing one, targeting theaccountapp's
grp_account_developergroup. The Setup entry is byte-unchanged, so admins keep the pagewhere the shipped guide and the runtime's own error text point.
Nothing else moves: no backend change, no authorization change, no change to which permissions
exist, no gate added or removed anywhere, and
packages/platform-objectsis targeted by name,never edited. Executes the recorded ruling (option A, director seat decision batch #85,
2026-09-08, maintainer verbatim 「其他同意」).
Why this shape and not the others — each refused on measurement
group_integrations)setup.accessgate fires before the group gate, so dropping the group gate alone changes nothing; dropping both serves 14+ unrelated Setup surfaces (Users, Organization, Business Units, Branding, Feature Flags, …) to every signed-in user — far past what the ruling asked foraccount.app.tsentry gated onrequiresService: 'mcp'mcpservice registers unconditionally ininit()while this bundle registers behindisMcpServerEnabled()— so the entry outlives its page and 404s for every signed-in user onOS_MCP_SERVER_ENABLED=false{ type: 'component', componentRef: 'mcp:connect-agent' }mcp:connect-agentlives in objectui's SDUI widget registry, not the app-component registry those nav items resolve throughA navigation contribution registers exactly when the page registers, which is why both
entries live in this one bundle and neither carries a gate of its own — an opted-out deployment
still gets no page and neither entry.
The item-id question, answered from the fold
Both entries deliberately share the item id
nav_connect_agent. That is scoped, not acollision, read off
SchemaRegistryrather than assumed: contributions are keyed by targetapp (
appNavContributions, a Map keyed by app name) andapplyNavContributions(app)consults onlyget(app.name), so a nav item id is unique within one app's navigation tree. Nothing indexesit across apps — no id-keyed registry, no de-duplication by id — and the translation bundles are
keyed
apps.APP.navigation.NAV_ID, which makes one shared id two distinct keys. One destinationkeeps one identity.
The two items are separate object literals rather than one shared const: the fold
structuredClones the app but pushes...c.itemsby reference, so a shared literal wouldsit in two apps' navigation trees at once.
Acceptance — both halves measured over the real composition
Real
SETUP_APP/ACCOUNT_APP/SETUP_NAV_CONTRIBUTIONS, the realCONNECT_AGENT_UI_BUNDLE, the real fold and the real RBAC-by-route filter (RestServerwiththis repo's established stub-the-exec-context pattern from
packages/rest/src/meta-app-publish-gate.test.ts), driven frompackages/cli— the one packagethat depends on all four:
GET /api/v1/meta/apps/accountgrp_account_developerchildren =['nav_account_api_keys', 'nav_account_oauth_apps', 'nav_connect_agent']GET /api/v1/meta/apps/setupPERMISSION_DENIED,connect_agentabsent from the bodysetup.access+manage_platform_settingsGET /api/v1/meta/apps/setupnav_connect_agentpresent — positive control: the harness does reach the cardapps/setupnot committed.
packages/mcpdeclares no dependency on@objectstack/rest,@objectstack/objectqlor@objectstack/platform-objects, so a permanent both-halves pin cannotlive inside this PR's declared file surface, and this file deliberately does not reimplement the
fold or the filter. See Acceptance notes for where that pin belongs.
The committed pin, and proof it can fail
packages/mcp/src/connect-agent-account-nav.test.ts(7 tests) pins the half this package owns:the contribution aims at the ungated app and group; its item carries nothing the server-side nav
filter could strip for a permissionless caller (
requiredPermissions/requiresService/visible); the Setup entry is byte-unchanged; both contributions parse against the realNavigationContributionSchema; and — the load-bearing one — the bundle declares no permissionkey anywhere and no
appscollection, so it cannot reach the card by widening Setup instead.Two ablations, each committed-first, proven on disk, and restored with
git checkout HEAD --verified by hash equality against the HEAD blob plus an empty
git diff HEAD:setupinstead ofaccount—"app: 'account',"1→0,"app: 'setup',"1→2group: 'group_integrations'dropped from the Setup contribution (a top-level append escapes the group gate) — occurrences 1→0so the test never ran — a void reading, recorded rather than quietly retried, and replaced by the
parse-safe A2 above.
Acceptance notes
packages/cli/test/. Read frompackage.json,packages/cliis the only workspace package depending on@objectstack/mcp,@objectstack/rest,@objectstack/objectqland@objectstack/platform-objectsat once —the same argument
packages/cli/scripts/check-app-nav-i18n.mjsmakes in its own header forliving there ("
cliis the composition root … the ONLY workspace package that depends on alleleven Setup nav contributors at once"). It is outside this PR's declared file surface and
packages/cli/**has siblings in flight this round, so it is reported rather than taken.node packages/cli/scripts/check-app-nav-i18n.mjspasses (10 contributors, 54 mergedsetupnav ids, 4 locales). Measured reason it is silent on the new entry: the gate scopes itself to
APP_NAME = 'setup'andcontinues on any contribution whoseappdiffers, so it demands notranslation key for a contributed account item. Consequence, noted: no
apps.account.navigation.nav_connect_agentlabel exists in any locale, so the entry renders itsEnglish literal under
zh-CN/ja-JP/es-ESwhile the Setup twin renders translated. Thosekeys live in
packages/platform-objects/src/apps/translations/— outside this surface.app-nav-translation-parity.test.tscannot see it either (it walks statically declared navonly, and asserts the reverse direction for
STUDIO_APPalone).packages/mcp/src/plugin.ts:372andpackages/mcp/README.md:92, both directing users to "Setup → Connect an Agent" — becomes truefor non-admins with this change and needs no edit. Recorded so nobody files it twice.
scope: C authors a new panel, this adds a second nav entry to the existing page, and
account.app.tsalready shipsnav_account_api_keysin the exact group targeted here.currently blocked by MCP OAuth cannot complete on 17.3.0: plugin-auth passes
validAudiences, which @better-auth/oauth-provider 1.7.2 no longer reads — everyresource=request fails withinvalid_target … is not configured#16530 and narrowed by OAuth-connected MCP agents run under themcp_agent_data_*ceiling ∩ user, not "as yourself": a viewAllRecords manager sees 5 accounts / 0 opportunities over OAuth vs 9 / 23 over an API key #16549". Both are now closed, so that half of theargument has expired. The ruling does not rest on it — it rests on API keys being per-user
credentials — and re-grading is triage's, not this PR's.
mainmoved under this branch and brought a breaking retirement whose name collides withthe shape used here: feat(spec)!: retire the
type: 'page'list-view mount and itspageNamebinding #17298, "retire thetype: 'page'list-view mount and itspageNamebinding". Read rather than assumed — it touches
packages/spec/src/ui/view.zod.ts, notapp.zod.ts, its own FROM → TO table prescribes "reach the page from the app'snavigation:{ id: 'nav_sales_home', type: 'page', pageName: 'sales_home', label: 'Sales' }" as thereplacement, it calls
PageNavItem.pageName"the page mount that has always rendered", and itstates that the nav twin
validateNavTargetRefsis untouched.origin/mainis merged in hereand the affected slice was re-verified on the merge result.
Gates
58 families derived on the final diff with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each run withoutput redirected and
$?captured before any pipe, then reconciled with--ran:check:dual-build-cjs-loadsrefused its own prerequisite: it reads built output and 12 packagesoutside this diff's closure have no
dist(@objectstack/studio,@objectstack/client-react,the four connectors, …). Its own text says so — "⛔ This is NOT a pass: nothing was measured."
It needs a repo-wide
pnpm build, which is CI's run; CI checks out and builds fresh. NOTMEASURED, recorded as neither green nor red.
Plus the two the tool does not name:
pnpm lint— run repo-wide to completion(
eslint . --no-inline-config, 2m24s, exit 0), so no narrowing was claimed — andnode packages/cli/scripts/check-app-nav-i18n.mjs(exit 0, verdict line quoted above).Receiving package, re-run on the merge result:
pnpm --filter @objectstack/mcp test— 30 files,320 tests passed — and
pnpm --filter @objectstack/mcp typecheck. The new test file is proven tobe in the type-checked program rather than assumed:
tsc -p tsconfig.test.json --listFilescounts it 1 (and 0 in the main program, which is
tsconfig.json's ownexclude, unchanged).packages/specmoved on the incoming side, sopnpm --filter @objectstack/spec build && check:generatedran on the merge result too.Every heavy run went through
scripts/pm/os-verify-lock.shwithOS_VERIFY_LOCK_SLOT=issue-16746.维护者速读(草稿)
改了什么 — 「连接智能体」这个页面,以前只在「系统设置」里有入口。现在在每个人自己的
「账户 → 开发者」里也加了一个入口,就挨着已经在那里的「API 密钥」。设置里那个入口原样保留,
管理员看到的东西一点没变。
为什么改 — 我们对外说的是「Claude 只能看到和操作您自己有权限的数据」,而后台本来就是
「谁调用就发给谁」。但界面上只有管理员能走到那个按钮,所以普通销售、销售经理照着指南操作,
第一步就卡住了。运行时自己报的错误消息也在把他们指向一个打不开的页面。维护者 9 月 8 日已裁决
按这个方向做(路线 A)。
风险与代价(含回滚) — 后台、授权、对外承诺一个字都没动,没有新增或删除任何权限开关。⚠️ 一个小缺口:这个新菜单项目前没有
唯一的变化是「账户」应用里多了一个菜单项。已实测:没有任何权限的用户现在能看到这个菜单项,
而「系统设置」对他仍然是 403 拒绝——这两件事同时成立,是本次验收的全部内容。回滚就是删掉那
一个对象字面量,一次 revert,无数据迁移、无兼容包袱。
中文/日文/西班牙文标签(翻译文件在另一个包里,不在本 PR 范围),所以在非英文界面下它会显示
英文原文。不影响功能。
席位意见 — (留空,待席位定稿)
你要做的 — 只需确认一件事:把「连接智能体」放进每个人自己的账户页,而不是继续只放在管理员
的系统设置里,符合你 9 月 8 日的裁决意图。其余都是机械落地。
Generated by Claude Code
Generated by Claude Code