Repository navigation
Description
domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp
Filing gate: ③ the maintainer's order that names this work: #30894 (「都同意,立卡」, 2026-11-06), whose Expected asks for "a Revoke action … An administrator can see and revoke them for a person they manage", and whose triage audit 6682129286 says the server door the page needs, if one is missing, is filed to domain:server first. The withdraw itself is the maintainer's cloud#3606 ruling A (「撤销或撤回时删掉这条记录 … 控制台的撤回也随之真正生效」), quoted in packages/plugins/plugin-auth/src/mcp-token-revocation.ts. ⛔ Not a claim. Triage sets the grade and the lane. Suggested grade: p1 (#30894's grade; it is the half its Revoke needs).
Business reason. The consent page promises 「您可以随时在账户设置中撤销访问权限。」. Since #30946 the console can list the assistants a person authorized (and an admin can list them for a person), but it has no ledgered door to withdraw one, so the list would show a lost laptop's Claude Desktop with no way to turn it off.
Reader. The domain:server seat (@objectstack/plugin-auth: the route ledger, the client, an admin door), then the domain:ui seat holding #30894.
Dedupe. delete-consent (86 hits; the auth ones are #30345 and #30918, both closed, neither ledgers the withdraw), withdraw consent (12: #30918, #30345), authorized-clients revoke (1: #30918). New.
What is on origin/main 115a43ad16
POST /api/v1/auth/oauth2/delete-consentis published by better-auth (BETTER_AUTH_MOUNTED_SURFACE,auth-route-ledger.ts~L430) but has noAUTH_ROUTE_LEDGERrow and no@objectstack/clientmethod. Its neighboursoauth2/consent,create-client,delete-client,get-client(s)andpublic-clientall carrydisposition: 'sdk'rows (~L173-178).mcp-token-revocation.tsalready names it "the Console's withdraw": it deletes the consent row, and since fix(plugin-auth)!: revoke an MCP access token through its OAuth consent #30879mcpConsentAdmitsrefuses the token on its next request. So the effect is there; the door the console may call is not.- The client's
oauth.authorizedClients.list()doc says "Withdraw a grant withPOST /oauth2/delete-consentand the row'sid" (packages/client/src/index.ts~L5006), pointing at an unledgered route. - An administrator:
GET /api/v1/auth/admin/authorized-clients?userId=(feat(plugin-auth): read the AI assistants a person has authorized, and when each was last used #30946) lists a person's grants, but nothing withdraws one for them;delete-consentacts for the session user only.
Wanted
- A ledgered, client-exposed withdraw of one of the session user's own authorized assistants, by the row
idauthorized-clientsanswers (the lane picks: ledgerdelete-consentassdk, or an ObjectStack mount besideauthorized-clients). After it, that assistant's token is refused on its next MCP call. - The same withdraw for a person a platform administrator manages, behind the same gate as
admin/authorized-clients(security#1's rules on Auth: no ledgered read lists the AI assistants a person has authorized, records when one was last used, or lets an admin see a managed person's #30918), refused for anyone else with one indistinguishable answer. - Tests on the real plugin: a member withdraws only their own (another's id is refused and leaves it in place); an admin withdraws a managed person's; the withdrawn token is refused next call.
Unblocks: the Revoke half of #30894. Related: #30918 (PR #30946), #30345 (PR #30879), cloud#3606.
Activity
objectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 ·
domain:server· 2026-11-06T21:54Z⛔ Not a claim.
POST /oauth2/delete-consentis mounted (auth-route-ledger.ts, L430) but has noAUTH_ROUTE_LEDGERrow and no client method; its neighbours carrysdkrows (L173 on). No admin door withdraws for a managed person. Triage re-read the ledger onmain.domain:server(plugin-auth,client).pm:queue.admin/authorized-clients(Auth: no ledgered read lists the AI assistants a person has authorized, records when one was last used, or lets an admin see a managed person's #30918, security#1's rules). A refusal for anyone outside that scope gives one indistinguishable answer, as the card says.domain:ui#3): addBlocked-by: #31048to The OAuth consent page promises you can revoke access from your account settings, but no page lists the AI assistants you authorized #30894 if its Revoke half waits on this card, so the unlock sweep and H14 see the order.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH
Claim: PM loop round 3 · 2026-11-07T00:27Z
Session:
session_01L9qibyvxbrLP4jm4PargoHAccount:
os-tesla(the card's assignee)Branch:
claude/project-thread-h42d2s(the card thread's designated branch; dev sessionsession_01L9qibxU7nr4BXM7e8qMfwq)Worktree:
objectstack-issue-31048Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-auth/src/**(the route ledger, the authorized-clients read and a withdraw beside it, theauth-plugin.tsmount lines, their tests),packages/client/src/index.ts(theoauth.authorizedClientsmethods and the staledelete-consentdoc line) and its test,packages/qa/dogfood/test/(one HTTP pin plus the admin non-admin refusal list if it enumerates the route), thecontent/docs/**pages that list the authorized-clients doors, one changeset. ⛔packages/spec/**(a new published key or error code stops and reports), ⛔ui/**(#30894's half), ⛔skills/**.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: highClause-②: yes (widening)
Metadata-protocol: none
Responsibility: the missing door is ours (
plugin-auth+client, objectstack):POST /oauth2/delete-consentis mounted with no ledger row or client method, and no admin door withdraws for a managed person. It reaches every person whose authorized assistant must be turned off (cloud#3606 ruling A).Thread-read: 6696803075
Premise: holds. On
origin/main23cf11a4auth-route-ledger.tslistsPOST /api/v1/auth/oauth2/delete-consentonly in the mounted surface (L417); the ledger has rows forGET /authorized-clientsandGET /admin/authorized-clients(L198–199) and none for a withdraw.Serial constraints cleared: none open; #30946 (#30918, the admin read and its gate) landed
6bd00d2a.Order:
/mnt/project-files/server-seat-2/r2026-11-06/dispatch-31048.md(frameFRAME.mdbeside it). The ownership measure of better-auth'sdelete-consentcomes first; a defect there routes to security#1 rule-level only.objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · os-tesla · session_01L9qibxU7nr4BXM7e8qMfwq
os-dev-report
{ "issue": 31048, "status": "done", "branch": "claude/project-thread-h42d2s", "pr": "https://github.com/objectstack-ai/objectstack/pull/31073", "session": "session_01L9qibxU7nr4BXM7e8qMfwq", "premise_still_valid": true, "summary": "Design (b): two ObjectStack mounts in the new plugin-auth authorized-clients-withdraw.ts. POST /api/v1/auth/authorized-clients/withdraw { id } withdraws a consent of the session user only; POST /api/v1/auth/admin/authorized-clients/withdraw { userId, id } runs the same gateAdmin as GET /admin/authorized-clients before any body read. Both revoke through revokeMcpGrant (the /oauth2/revoke owner: the consent row, then the client's refresh family, via better-auth's adapter, attributed to the withdrawer), so mcpConsentAdmits refuses the token on its next MCP call. Unknown and not-yours give one byte-identical 404 RESOURCE_NOT_FOUND. Ledgered sdk (oauth.authorizedClients.withdraw / withdrawForUser, added to @objectstack/client); better-auth's /oauth2/delete-consent ledgered server-only with the measurement. Measured on @better-auth/oauth-provider 1.7.12: delete-consent DOES check ownership (consent.userId !== session.user.id), so there is no security defect, but it answers unknown 404 and another's 401 (distinguishable) and leaves the refresh family, which is why (a) was not taken. Audit: grant and revoke write no explicit row; plugin-audit's generic writer records the consent create/delete (sys_oauth_consent not in SKIP_OBJECTS), and the withdraw inherits the same generic delete row with no new action value, measured in dogfood with user_id = the withdrawer.", "tests": "On head 1c32be06 (main 5210acfb merged): plugin-auth `pnpm --filter @objectstack/plugin-auth test` Test Files 232/232, Tests 4187 passed | 10 skipped; real-plugin + ledger conformance 26/26; client `pnpm --filter @objectstack/client test` 55/55 files, 712/712 (cdf571a0; merge touched no client file), touched client file 8/8 on the final head; plugin-auth + client typecheck exit 0; dogfood after a full pnpm build (119/119): authorized-client-withdraw 2/2, admin-route-nonadmin-refusal + admin-platform-admin-standing 15/15; eslint --no-inline-config on changed .ts exit 0. Ablations via scripts/ablation-replace.mjs against the committed fix, src-resolved suite (plugin-auth tests import ./auth-plugin and ./authorized-clients-withdraw source, no dist): (i) ownership check removed from findConsentOf -> 2 red ('another person's id is refused with the SAME bytes…' expected 200 to be 404; 'a consent that is not the named person's…' expected 200 to be 404), restored blob == HEAD 8835068a7d09, git diff HEAD empty; (ii) gateAdmin removed from the admin withdraw mount -> 1 red ('a member, an organization OWNER…' member: expected 200 to be 403), restored blob == HEAD 61ed5aefa975, git diff HEAD empty.", "gates": "node scripts/pm/dispatch-gates.mjs --commands on 1c32be06: 95 derived, 95 run, all exit 0, each with </dev/null and the exit code captured before any pipe; --ran reconciliation: 95 run, 0 NOT-MEASURED, 0 UNRUN (record /mnt/project-files/server-seat-2/r2026-11-06/out/31048-gates-ran-1c32be06.list). First sweep on 9bcc1b5e: check:doc-authoring exit 1 (cloud#3606 in a ledger note string; removed in cdf571a0), check:skill-examples and check:dual-build-cjs-loads exit 3 (no dist before the full build), check-plugin-teardown-shape --self-test exit 3 (shallow clone; pinned fixture commit fetched); all exit 0 on the final sweep. pnpm lint family: eslint --no-inline-config on the changed .ts files exit 0; the repo-wide pnpm lint is CI's. PR check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get #31048, get_comments #31048 twice); 0 writes", "api_writes": "2 — fleet-write relay pr_create (objectstack-ai/objectstack#31073, read-back identical 11632 bytes); this os-dev-report comment via post-stamped. The harness's assignee/reviewer step was skipped on the frame's rule.", "line_budget": "15 files, 991 insertions / 18 deletions vs merge base 23cf11a4 (under 3000; no governed path)", "files_changed": "packages/plugins/plugin-auth/src/{authorized-clients-withdraw.ts (new), auth-plugin.ts, auth-route-ledger.ts, auth-route-ledger.conformance.test.ts, authorized-clients-read.ts, authorized-clients-read.real-plugin.test.ts, mcp-token-revocation.ts, auth-manager.ts}; packages/client/src/{index.ts, oauth-authorized-clients.test.ts}; packages/qa/dogfood/test/{authorized-client-withdraw.dogfood.test.ts (new), admin-route-nonadmin-refusal.dogfood.test.ts, admin-platform-admin-standing.dogfood.test.ts}; content/docs/ai/connect-mcp.mdx; .changeset/31048-withdraw-authorized-assistant.md", "docs_pages": "No content/docs page listed the authorized-clients doors before this change. One bullet added to content/docs/ai/connect-mcp.mdx, in The security model.", "answer_table": "own door: own id 200 { id } | another's id 404 RESOURCE_NOT_FOUND (same bytes as unknown; row stays) | unknown id 404 RESOURCE_NOT_FOUND | a platform admin on the own door with another's id 404 (keyed by session). admin door: platform admin with { userId, id } of that person 200 | id not that person's 404 (same bytes as unknown) | userId naming no user 404 with the admin read's message | non-admin (member, org owner, org admin, the person) 403 PERMISSION_DENIED, byte-identical for existing, missing and empty bodies. Anonymous 401 UNAUTHENTICATED on both. Shape: query param / foreign key / missing, blank or non-string value / non-object 400 VALIDATION_ERROR; unparseable JSON 400 INVALID_REQUEST.", "deviations": "Changeset and commit trailers are model-free (Co-authored-by: Claude) per AGENTS.md, which overrides the harness's model-named trailer. Ablations ran on the src-resolved unit suite, not dogfood. The refresh delete is not organization-scoped (inherited from revokeMcpGrant); see Acceptance notes.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": ["carrier: none · noted, not filed — revokeMcpGrant (and so /oauth2/revoke and this withdraw) deletes the client's refresh family across organizations although oauthRefreshToken carries referenceId; a person with the same client granted in two organizations re-authorizes the other organization after a withdraw. Recorded in the PR's Acceptance notes."] }objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH
ACCEPT pointer · 2026-11-07T02:42Z · PR #31073 (a person withdraws their own authorized AI assistant, and a platform administrator withdraws a managed person's, through two ledgered doors and
@objectstack/clientmethods; another person's or an unknown id gets one 404; the token is refused on its next MCP call) accepted at head05ade983in 6701165909; readied with auto-merge. The organization-unscoped refresh-family delete is filed as #31076.objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH
Landing · 2026-11-07T03:03Z · PR #31073 MERGED 2026-11-07T03:03Z as
e11a5759(Fixes #31048). There are two new doors:POST /api/v1/auth/authorized-clients/withdraw{ id }withdraws the person's own grant, andPOST /api/v1/auth/admin/authorized-clients/withdraw{ userId, id }withdraws a managed person's grant behind the admin read's own gate. The client spells themoauth.authorizedClients.withdraw(id)andwithdrawForUser(userId, id). The assistant's token is refused on its next MCP call. The seat dropspm:dispatchedandpm:blocking. Follow-up: #31076 (the refresh-family delete is not scoped to the organization). #30894's Revoke half is unblocked.