Repository navigation
fix(runtime,mcp): list_actions marks an organization action the caller's membership role does not reach - #33194
Conversation
…r's membership role does not reach The MCP listing keeps every exposed identity action and marks the ones whose organization endpoint the caller's role cannot reach as unavailable, with the code run_action answers (PERMISSION_DENIED) and a sentence naming the roles that reach it. The reach is read from the action's target against the spec's MEMBERSHIP_REACH table; neither run door reads the mark. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvdpdhupF2wQf2NUU8
… changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvdpdhupF2wQf2NUU8
…ip-mark read The new list_actions mark skips the system context, which is an elevation read the system-context census counts. Row 62b names it, and the generated site register is regenerated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvdpdhupF2wQf2NUU8
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 37 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 1602ffc8e1ca3058aab9529739d9d11d5fba0114 && git checkout 1602ffc8e1ca3058aab9529739d9d11d5fba0114
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f45c0125143c766d1d7900590747ad8acded344e f91b2a1d69f7bc0dd466089d23ba6aa844ff0042 && git checkout -B drift-repro f45c0125143c766d1d7900590747ad8acded344e && git merge --no-ff f91b2a1d69f7bc0dd466089d23ba6aa844ff0042
node scripts/docs-audit/affected-docs.mjs --json f45c0125143c766d1d7900590747ad8acded344e
|
The list_actions row named only the credential mark. It now names the three codes a listed action can carry, the membership-grade mark among them, and the intro and "What gets exposed" sentences no longer say every listed action can be run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvdpdhupF2wQf2NUU8
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx 维护者速读 · PR #33194 @
|
Requested by Tesla · project thread
Fixes #32163
Clause-②: no
Metadata-protocol: none
维护者速读
list_actions看到的 15 个组织类身份动作(邀请成员、移除成员、改组织、管团队等)仍然列出,但每行带上unavailable标记,写明需要哪种组织角色(如"需要组织 owner、admin 或 delegated admin")。owner、admin(邀请类还有 delegated admin)看到的列表不变。packages/runtime/src/domains/mcp.ts的membershipReachRefusal与listActions里refusal那一行的顺序;packages/runtime/src/platform-admin-action-gate.test.ts末尾新 describe。pnpm --filter @objectstack/runtime exec vitest run src/platform-admin-action-gate.test.ts,预期 16 passed。What changes
listActions(packages/runtime/src/domains/mcp.ts) asks a new module function,membershipReachRefusal(action, objectName, ec), after the two existing refusals:confirmation ?? apiForwardCredentialRefusal(...) ?? membershipReachRefusal(...). That is the orderrun_actionmeets them: the confirmation gate in the admission sequence, the forward's credential table before the target is called, and the organization endpoint last.target. When it is an organization endpoint under the auth base path (/api/v1/auth/organization/...), the grades that reach it are the union of the grades of everyMEMBERSHIP_REACHrow naming that endpoint (@objectstack/spec/identity, the table plugin-auth'smembership-reach-table.test.tspins equal to the vendor doors)./organization/update-member-roleis two rows, so its reach is their union. No local list, and no new stored key: the reach is derived from what survives on the served action (itstarget), so ruling A's key is not needed.positionsin the organization the listing reads, by the namesmapMembershipRoleprojects the grades to (org_owner,org_admin,delegated_admin). That is the reading the actions'requiresMembershipReachsugar is lowered to. Only an envelope that carries its own positions is judged: the system context, a delegated agent (its envelope carries the agent ceiling,positions: [], not the person's grades) and an envelope with nopositionsarray are left unmarked.unavailable: { code: 'PERMISSION_DENIED', reason }. The code is the onerun_actionanswered for a member on every set-A/B action measured on a booted showcase (below). The reason names the action, the roles that reach it and the endpoint, and says who can run it: "Action 'invite_user' on 'sys_member' needs an organization owner, admin or delegated admin: its endpoint (/organization/invite-member) refuses every other membership role, and your role in this organization is none of those. Ask a person who holds one of those roles to run it."run_actionandPOST /actionsforward the call to the endpoint exactly as before, and the endpoint stays the authority (a call naming another organization is judged there).content/docs/permissions/system-context.mdxowns the mark's system-context skip (an elevation read the census counts), and the generated site register namesmcp.ts#membershipReachRefusal.content/docs/ai/actions-as-tools.mdx: thelist_actionsrow names the three codes a listed action can carry (UNAUTHENTICATED,ACTION_CONFIRMATION_REQUIRED, andPERMISSION_DENIEDfor the membership grade), and the intro and "What gets exposed" sentences no longer say every listed action can be run.McpActionSummary.unavailable's JSDoc and thelist_actionstool description (packages/mcp/src/mcp-http-tools.ts) name the membership-role case and its code, and the description no longer promises that every returned action can be run ("that the caller's permissions allow are returned"; "cannot be run by you as you are connected (your credential, or your role in this organization)"). The served skill (packages/mcp/src/skill-md.ts) no longer says "only those … you are permitted to run" and says whatunavailablemeans. Its Console mirror (ui/apps/console/src/pages/developer/objectstack-skill.ts) carries the same bytes, as its two drift pins require.Not marked (sets C and D, listed and refused at their door as today):
sys_sso_provider.delete_sso_providerandsys_oauth_application.delete_oauth_application(judged on record ownership), andsys_organization.create_organization(judged on the deployment's tenancy posture; measured on the showcase:403 ORGANIZATION_CREATION_DISABLED). No reach row names their endpoints, so the function answersundefinedfor them.Tests
New top-level
describe('[ruling E] list_actions marks an organization action the caller's membership role does not reach')inpackages/runtime/src/platform-admin-action-gate.test.ts, the identity-action harness: the REAL identity declarations (@objectstack/platform-objects/identity) through the productionMcpActionBridge, with the forward target faked as the endpoints answer a refused role.api-typed, and declares no capability gate.positions: ['org_member', 'everyone']) is listed every set-A and set-B action, each markedPERMISSION_DENIED, the reason naming "owner or admin" (set A) or "owner, admin or delegated admin" and/organization/invite-member(set B).run_actionunchanged. For each of the 15, the member's call is forwarded (the fake endpoint is called once, so no door refused first) and refused with the endpoint'sPERMISSION_DENIED/ 403 and its sentence; the admin's call is forwarded once and served. Green atmaintoo.Red at
main(test file at this branch,packages/runtime/src/domains/mcp.tsrestored from28a2f0f5): 2 failed / 14 passed (cases 2 and 3: no mark). Green here: 16 / 16.Ablations (each through
scripts/ablation-replace.mjs, anchor 1 → 0, restored to the HEAD blob,git diff HEADempty after each; the suite imports./http-dispatcher.jsfromsrc, so nodist/sits between mutation and run)if (false) return undefined;)listActions(?? undefined)if (!served || membershipReachRefusal(...)) continue;)Live measurement on a booted showcase (temporary dogfood file, not committed; MCP over the session bearer of a signed-up plain member and of the administrator)
At
origin/main28a2f0f5: the member is listed 34sys_*actions, all 18 of sets A–D among them, none marked; the administrator 44. As the member,run_actionanswers403 PERMISSION_DENIEDwith the endpoint's own sentence on 14 of the 15 set-A/B actions ("You are not allowed to invite users to this organization", "… to delete this member", "… to update this organization", "… to create teams in this organization", …).cancel_invitationanswered400 VALIDATION_ERROR"Member not found": the only invitation row the fixture holds belongs to another organization, so the endpoint stops before its role check. Set C/D:create_organization403 ORGANIZATION_CREATION_DISABLED; the two deletes404(no such provider or client in the fixture).At this branch (
d4df05f4, after the rebuild): the member is listed the same 34, the 15 set-A/B rows markedPERMISSION_DENIEDand nothing else; the administrator the same 44, none marked. Every memberrun_actionanswer is identical tomain's, code, status and sentence.Records:
measure-main-28a2f0f5.jsonandmeasure-head-d4df05f4.jsonin the seat'sout/32163/.Acceptance notes
ruling|Ruling-ref|maintainer|⛔) over the edited files:mcp.ts17 hits,mcp-http-tools.ts39,skill-md.ts1, none in or governinglistActions, theunavailableJSDoc or thelist_actionsdescription and skill lines; the one adjacent is therun_actionlisted-check ("maintainer's ruling on the card: no bypass"), which this change does not touch.positions, as the loweredvisiblepredicate does. Two edges where it can differ from the endpoint, both in the safe direction for running (the mark never stops a run): a position granted directly throughsys_user_positionunder a grade name (no mark, the endpoint still refuses, as today); asys_memberrow outside its ADR-0091 validity window (no position, so marked, while the vendor may still admit). With no active organization, every membership's grades count, so a caller who is an admin anywhere is not marked.PERMISSION_DENIEDis a constant beside the function, typed as the registered vocabulary. It is whatrun_actionanswered for a member on the showcase; the reason is this listing's own sentence, not the endpoint's (each endpoint words its refusal differently, and the table does not carry them)./actionslisting is not changed: the ruling scopes the mark to MCPlist_actions.Verification
Package suites, typecheck, lint (at `056f314d`; later commits are docs-only)
After the last merge of
main(056f314d), on a fresh install and a full build (declarations included) of the@objectstack/runtime...,@objectstack/mcp...and@objectstack/service-storage...closures:pnpm --filter @objectstack/runtime exec vitest runpnpm --filter @objectstack/mcp exec vitest runpnpm --dir ui exec vitest run apps/console/src/pages/developer/objectstack-skill.test.tspnpm --filter @objectstack/runtime --filter @objectstack/mcp typecheckpnpm lint3de7508e(system-context row) andf91b2a1d(actions-as-tools page) add only docs on top of056f314d; the package suites were not rerun for them.Derived gates (dispatch-gates --commands; reconciled with --ran)
dispatch-gates --ranatf91b2a1d: 100 derived, 100 run, 0 NOT MEASURED, 0 unrun. Record:gates-ran-f91b2a1d.listin the seat'sout/32163/. The docs-family gates (31,check:role-wordandcheck:system-context-censusamong them) were rerun atf91b2a1d; the rest were run at3de7508e, which differs from it by one docs page.node scripts/check-plugin-teardown-shape.mjs --self-testfirst exited 3: its positive control is pinned to commit621a4876, which the shallow clone did not hold. Aftergit fetch --depth=1 origin 621a4876…it passes (48 cases).pnpm check:system-context-censuswas red at the first gate run: the mark's system-context skip is an elevation read no row owned. Fixed in3de7508ewith row 62b oncontent/docs/permissions/system-context.mdxand the regenerated register; green after.pnpm check:role-wordwas red on the first wording of that row ("membership role"); the row now says "membership grade". Green after.Generated by Claude Code