Skip to content

feat(console): revoke an authorized AI assistant from Connect an Agent, and an admin widget for a person's assistants - #31084

Queued
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-ulycg0
Queued

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-ulycg0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Part of #30894 (part 2: the assistants' Revoke, plus the ui half of the administrator view. The card stays open for the server half: one "AI Assistants" tab on the sys_user record page that names the widget added here, filed by the seat.)

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:「连接 AI 助手」页里已授权的每个 AI 助手旁新增「撤销」按钮,确认后立即收回该助手的访问权限并刷新列表;删掉「暂不支持在此页面撤销」那行提示。另新增一个管理员组件 mcp:authorized-assistants,放在用户记录页上即可查看并撤销此人授权的助手(目前还没有页面引用它)。
  • 为什么改:OAuth 授权页承诺「可以随时在账户设置中撤销访问权限」,第一部分只做了列表;服务端撤销接口 Auth: the console cannot withdraw an authorized AI assistant: /oauth2/delete-consent has no ledger row or client method, and no admin door #31048 已合并,这一步把承诺补完整,并为管理员替员工撤销准备好界面。
  • 风险与回滚:只调用已上线的两个撤销接口(本人 / 平台管理员),每次撤销前都要确认;管理员组件在页面引用前不会出现。回滚即还原本 PR,无数据迁移。
  • 先看:ui/packages/app-shell/src/console/connect/AuthorizedAccessList.tsx(撤销流程与撤销后重读)、ManagedPersonAssistants.tsx(只从记录上下文取用户 id)、ui/packages/i18n/src/locales/en.ts(新增 5 个、删除 1 个 connectAgent.access.* 键)。
  • 两分钟验证:pnpm dev 后以授权过助手的用户登录,打开 账户 → 连接 AI 助手,在某个助手上点「撤销」并确认,该行消失,该助手下一次 MCP 调用被拒绝(401)。
  • 需要你做的:无,席位落地。

What changes

  • Assistant Revoke (AuthorizedAccessList.tsx). The assistants half is now its own AuthorizedAssistantsSection. Every assistant row carries a Revoke. It asks first, naming the assistant ("Revoke access for Claude Desktop?", the unnamed fallback for a nameless client), then calls oauth.authorizedClients.withdraw(id) with the row's consent id. The list is read again after every withdraw, answered or refused: a refusal is most often a grant already withdrawn elsewhere (the server answers 404 for it), so its message is shown and the stale row leaves. Revoke buttons wait while a withdraw is in flight. The connectAgent.access.withdrawLater line is removed. The API-key half is unchanged.
  • Administrator view, ui half (ManagedPersonAssistants.tsx, new). SDUI widget registered ONCE as mcp:authorized-assistants (namespace: 'mcp', skipFallback: true, so no bare authorized-assistants and no app-shell: spelling). It reads className from the node and one thing from the page: the bound record from useRecordContext(), which must be a sys_user record with an id. It takes no props. It renders AuthorizedAssistantsSection with forUserId = that id, which lists through oauth.authorizedClients.listForUser(userId) and revokes through withdrawForUser(userId, id), with the same rows and confirm. Off a sys_user record (Studio palette, any other object's page, no id) it renders nothing. Those doors are platform-admin only; anyone else gets the server's 403 shown as a failed read, never as an empty list. No page names the widget yet, so nothing changes on screen until the server card (Setup user page: add an "AI Assistants" tab that shows and revokes the assistants a person authorized #31085) adds the tab.
  • Its @object-ui/types arm (zod/mcp-authorized-assistants.zod.ts McpAuthorizedAssistantsSchema, TS twin mcp-authorized-assistants.ts), a member of AnyComponentSchema / AnySchema, so objectui validate claims the type instead of refusing it at type (the registered-types ratchet's namespaced pin stays 306). Read from the widget's read points: properties may only be {}; body / children are refused by name with the family clause. Pinned by mcp-authorized-assistants-arm.test.ts; the mirror-parity pair count moves 197 → 198 and the emit-headroom table gains the arm.
  • ui/packages/app-shell/src/index.ts imports the widget for its registration, and package.json sideEffects lists it (dist and src), as for ConnectAgentWidget.
  • ui/packages/cli/src/utils/known-schema-types.ts regenerated (node scripts/regenerate-known-schema-types.mjs): one line, mcp:authorized-assistants. The 10859 ratchet gains a row pinning that it registered with its arm.
  • Language packs, all eleven: added connectAgent.access.withdrawTitle, withdrawBody, managedBody, managedEmpty, managedTruncated; removed withdrawLater. Net +4 keys per pack; no existing key reworded. zh-Hant regenerated from zh-Hans with gen:zh-hant.
  • Changeset .changeset/30894-assistant-revoke.md (@object-ui/app-shell, @object-ui/i18n, @object-ui/types, @object-ui/cli: patch).

Tests

AuthorizedAccessList.test.tsx, 21 cases (12 before). New: every assistant row has a Revoke; Revoke asks first naming the assistant, Cancel withdraws nothing; a nameless assistant is named by the fallback; confirm calls withdraw('consent_2'), re-reads the list, the row leaves, keys are not re-read and withdrawForUser is not called; a refused withdraw (grant gone) names the refusal and still re-reads. §5, the admin widget: registered as mcp:authorized-assistants; on a sys_user record it lists through listForUser('user_mia') (never list) and revokes through withdrawForUser('user_mia', 'consent_1'), then re-reads; empty and truncated lines speak of "this person"; a refused read (not an admin) is named, not shown empty; off a sys_user record or without an id it renders and reads nothing. The old "no Revoke, withdrawLater shown" case is replaced by the Revoke cases.

Ablation (one-off, on committed head 5087922): replacing the admin branch's withdrawForUser(forUserId, client.id) with withdraw(client.id) turned §5's revoke case red (1 failed, 20 passed); restored and verified git diff HEAD empty by scripts/ablation-replace.mjs.

Acceptance notes

  • Where the administrator view lands. Setup shows one user only on the sys_user record page, which is server metadata (packages/platform-objects/src/pages/sys-user.page.ts, kind: 'full', its own tab strip). The console has no ui-side way to add a tab there, so this PR ships the widget and the server card adds the tab: a page:tabs item whose body is { type: 'mcp:authorized-assistants', properties: {} }. The widget needs nothing from the page but the bound record. Gating the tab to platform administrators is the server card's call; without it, a non-admin sees the 403 named.
  • Shared files with Connect an Agent never says what an assistant can do once connected, or whether the connection worked #30898: ConnectAgentWidget.tsx is not touched. In the packs, keys are only added and one removed; no existing string is reworded.
  • Refusal text: a refused withdraw shows the server's message ("No authorized assistant you may withdraw matches the supplied id.") under the list, and the list is re-read.
  • Follow-up plugin-auth: withdrawing an AI assistant in one organization also deletes its refresh tokens in the person's other organizations #31076 (refresh-family delete not scoped to the organization) is server-side and does not change what the console calls.

Verification

Both withdraw doors, live on origin/main cb8f520 (showcase, fresh; admin + two members)
  • Own door POST /auth/authorized-clients/withdraw: the member's own grant 200 {id}, and the assistant's next MCP call 401 (200 before). Another member naming that id: 404 RESOURCE_NOT_FOUND, and the assistant's MCP call still 200. Unknown id 404; the same id a second time 404; missing id 400 VALIDATION_ERROR. The admin naming the member's id on the own door: 404.
  • Admin door POST /auth/admin/authorized-clients/withdraw: member caller 403 PERMISSION_DENIED; admin with the wrong userId 404; admin with the member's userId 200 {id}, the assistant's next MCP call 401, and listForUser then answers [].
  • A user created through admin/create-user answers 403 PASSWORD_RESET_REQUIRED on these doors until they change their password (the server's existing rule, not new here).
Console, live (Vite dev against that backend), head 2583f17's code

As a member with two grants (Claude Desktop, Cursor), on Account → Connect an Agent: both rows show a Revoke and the not-available-yet line is gone. Revoke on Claude Desktop opened "Revoke access for Claude Desktop? The assistant loses access at once. To use it again, it has to be authorized again."; confirming removed the row (Cursor stays). Claude Desktop's token then answered 401 on MCP while Cursor's still answered 200. The admin widget is not reachable live yet: no page names it (unit-tested, §5).

Local gates and tests
  • pnpm --dir ui exec vitest run packages/app-shell/src/console/connect/: 2 files, 37 tests passed. With packages/i18n/, the plugin-form affordanceGrantMap-12082 write census and the product-neutral ratchet: 102 files, 1859 passed, 13 skipped. After merging origin/main (2583f17): connect + affordanceGrantMap + check-side-effects-array: 4 files, 138 passed.
  • pnpm --filter @object-ui/app-shell type-check and @object-ui/i18n type-check: exit 0. eslint on console/connect/ and index.ts: 0 errors (3 existing warnings in ConnectAgentWidget.tsx, untouched).
  • node scripts/pm/dispatch-gates.mjs --commands derived 51 commands on the final change set; all 51 exit 0; --ran reconciliation: 51 derived, 51 run, 0 NOT-MEASURED. That includes node scripts/check-sdui-manifest.mjs exit 0 and pnpm --dir ui exec vitest run scripts/ (170 files passed, 2 skipped) on fec0386, which first caught the missing sideEffects entry (fixed in fec0386). check:type-check-debt exit 0 on 5087922 (the package.json change after it touches no typecheck script).
  • pnpm check:new-line-citations (ui): 0 new citations.
  • NOT MEASURED locally: the i18n-locale-en eager-closure ceiling (needs the Console production build; CI's Build & E2E UI check:eager-closure is the verdict). The ceiling is not changed.
CI fix round, head 5044eb2
  • Cause of the red Test UI (6/16): registered-types-validate-ratchet-10859.test.ts counted 308 refused namespaced keys against the pin 306 — mcp:authorized-assistants and app-shell:mcp:authorized-assistants had no arm. Fixed by arming the type and registering it under one key; the pin is unchanged.
  • pnpm --dir ui exec vitest run packages/cli/ packages/types/ packages/app-shell/src/console/connect/ packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts scripts/: 592 files passed, 2 skipped (15956 tests passed).
  • @object-ui/types, @object-ui/app-shell, @object-ui/cli type-check: exit 0. eslint on the new files: exit 0. ui check:new-line-citations: 0 new.
  • dispatch-gates --commands: 51 derived, all 51 exit 0, --ran 0 NOT-MEASURED (record: project files ui-seat-3/r2026-11-06/30894/gates-ran-p2-510c6e36.list); changeset gates re-run on 5044eb2 exit 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the touched files passes (the repo-wide pnpm lint is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qiby1cruv4PSfF5pFmit


Generated by Claude Code

… for a person's assistants

Assistant rows under Connect an Agent get a confirm-first Revoke through
oauth.authorizedClients.withdraw(id); the list is read again afterwards and
the not-available-yet line is removed. AuthorizedAssistantsSection is the
assistants half on its own; with forUserId it uses listForUser and
withdrawForUser, and mcp:authorized-assistants mounts it for the sys_user
record a page is bound to.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qiby1cruv4PSfF5pFmit
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 2583f17098a714a8fcf92c78b6046c0af235cc1d
Local-runs: none

① Derived judgments

  • The declaration names the whole widening: five new connectAgent.access.* leaves in all eleven packs (withdrawTitle, withdrawBody, managedBody, managedEmpty, managedTruncated) and one new SDUI type, mcp:authorized-assistants (app-shell namespace, inputs: []), listed in ui/packages/cli/src/utils/known-schema-types.ts (both spellings) and in app-shell's sideEffects. git diff --stat origin/main...2583f170: 18 files, +459/−97. Nothing existing is reworded.
  • One key leaves, connectAgent.access.withdrawLater. This is not a published narrowing: it was added by feat(console): list the AI assistants you authorized and your API keys under Connect an Agent #31054, whose changeset .changeset/30894-authorized-access-list.md is still pending on main, so no released version carries it. Its two readers on main (the component and its test) are both in this diff.
  • Doors used as served by feat(auth): withdraw an authorized AI assistant, for the person and for a platform administrator #31073 (e11a5759): oauth.authorizedClients.withdraw(id) for the person's own grant, and listForUser / withdrawForUser for the admin widget. The dev measured them live: own 200 then MCP 401; stranger, unknown or repeat 404; member on the admin door 403. Reading: ui-seat-3/r2026-11-06/30894/reading-withdraw-cb8f5200.md.
  • The new widget reads only the bound record (useRecordContext()), must be a sys_user record with an id, takes no props, and renders nothing anywhere else. No page names it yet; the sys_user page tab is server metadata (packages/platform-objects/src/pages/sys-user.page.ts), filed as the seat's follow-up.
  • No packages/** file. ConnectAgentWidget.tsx is untouched (seat 2's Connect an Agent never says what an assistant can do once connected, or whether the connection worked #30898 serial ask kept).

② Semver level

@object-ui/app-shell patch and @object-ui/i18n patch, Clause-②: yes (widening), Metadata-protocol: none: additive keys, an additive control and an additive registered type. The one removed key was never released. Correct.

③ Boundary flags

None. The admin view's page tab is outside ui/ and is not in this PR; Part of #30894 is right, because the card's "an administrator can see and revoke them" half closes only when a page names the widget.

④ Design judgment

The Revoke asks first, names the assistant, and re-reads the list after every withdraw, answered or refused, so a grant withdrawn elsewhere also leaves. Registering the admin widget ahead of the page that names it follows the landing order (consumer after provider) and is harmless until named.

Implemented-by: claude/project-thread-ulycg0
Reviewed-by: session_01L9qibxW6qraU2Z3mwupUGp

VERDICT: PASS

Registered once as mcp:authorized-assistants (no bare or app-shell spelling),
with its zod arm and TS twin: the widget reads only className, so the
properties bag is {} and both content channels are refused by name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qiby1cruv4PSfF5pFmit
@github-actions github-actions Bot added the tests label Nov 7, 2026
…mily clause; count the new mirror pair

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qiby1cruv4PSfF5pFmit
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

Contract review (delta)

Served-tier: TIER_DEFAULT
Head-sha: 5044eb29d3fc4382d2124fe44c72c00373ec393f
Local-runs: none
Scope: git diff 2583f170 5044eb29 (3 commits f9bfb8f, 510c6e3, 5044eb2; 12 files, +298/−9), read in full; the PR body as patched to match.

① Derived judgments

  • Cause of the red: "Test UI (6/16)" failed registered-types-validate-ratchet-10859.test.ts:586, 308 refused namespaced keys against the pin of 306. The two extra keys were mcp:authorized-assistants and app-shell:mcp:authorized-assistants, registered without an arm. The fix keeps the pin at 306, as that test requires.
  • The registration moves to namespace: 'mcp' with skipFallback: true, so the registry now has one key, mcp:authorized-assistants. That is the spelling a page authors and the one known-schema-types.ts keeps. The app-shell: spelling is gone. A new ratchet row pins "one key, armed, not refused".
  • New arm McpAuthorizedAssistantsSchema (zod/mcp-authorized-assistants.zod.ts) and TS twin (mcp-authorized-assistants.ts) mirror the precedent arm CloudWorkspaceTimezoneNoticeSchema. They extend BaseSchema, properties is a strict {}, and body/children are retirementTombstone refusals. These match the widget's read points: it reads only className (now forwarded through cn) and the bound record. The arm is a member of AnyComponentSchema and AnySchema, it is exported from the zod barrel and the entry, it is counted in mirror parity (197 → 198, the test's own instruction for an added pair), and it has a row in the emit-headroom table.
  • Changeset now also names @object-ui/types and @object-ui/cli (patch).

② Semver level

Unchanged: Clause-②: yes (widening) (a new union member and a new export are additive), Metadata-protocol: none (no packages/spec), patch in the fixed group. Correct.

③ Boundary flags

None.

④ Design judgment

Arming the type in the same change as its registration is the landing order the ratchet asks for, and the single mcp: key removes a stray app-shell:mcp: spelling no page would author. Non-blocking nit: ui/packages/types/src/zod/README.md lists the new arm under "### Cloud Widgets (1)". That heading's name and count now undersell it; it can be renamed in a later README pass.

Implemented-by: claude/project-thread-ulycg0
Reviewed-by: session_01L9qibxW6qraU2Z3mwupUGp

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

维护者速读

席位核对 — ACCEPT · head 5044eb2

  • 形态:27 个文件,+749/−98。check-governed-merges --pr 31084:NOT governed,847 行 ≤ 3000。Part of #30894,目标 main。与认领 6701992250 及修订 6702008412(分支)一致。②的管理员视图按席位裁定拆分:组件在本 PR,页面标签页在 Setup user page: add an "AI Assistants" tab that shows and revokes the assistants a person authorized #31085。ConnectAgentWidget.tsx 未动。本 PR 由本席派出的卡线程实现,席位不写码。
  • 契约:Clause-②: yes (widening)(新增 5 个键、1 个 SDUI 类型),Metadata-protocol: none,app-shell + i18n + types + cli patch。去掉的 withdrawLater 由第一部分引入,其 changeset 尚未发布,不构成已发布面的收窄。TIER_DEFAULT 复核 6702453133(2583f170)PASS;CI 修复轮的增量复核 6703056229 在本 head 上 PASS:为 mcp:authorized-assistants 补上 @object-ui/types 的 zod 臂与 TS 孪生,注册改为单一 mcp: 键,registered-types 棘轮仍为 306,未抬高。changeset 增列 types、cli patch。
  • 席位自己核对:撤销只调 withdraw(id),先确认;管理员组件只读绑定记录,非 sys_user 记录不渲染;非管理员看到服务端 403 的读取失败,不是空列表。
  • 前提实测:dev 报告 6702437784(status done)。现场实测两个撤回接口(本人 200 后 MCP 401;他人/未知/重复 404;成员调管理员接口 403),控制台撤销后助手令牌 401。派生门禁 51/51 exit 0。
  • 门禁 check-runs:head 5044eb2 上 60 个 check-run:47 success、13 skipped(路径过滤),0 red,全部完成;含 Test UI 全 16 片与 Build & E2E UI(eager-closure)。mergeable: clean。
  • 落地条款:维护者常设令 「你应该是使用 pm-dispatch 技能自主审核和落地 pr」(2026-11-06T14:09Z)。中继 pr_ready + automerge_enable。Part of:合并后 The OAuth consent page promises you can revoke access from your account settings, but no page lists the AI assistants you authorized #30894 转 pm:blocked(正文加 Blocked-by: #31085),清 assignee。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 7, 2026 06:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant