Repository navigation
identity: the platform-admin-gated actions on sys_user, sys_oauth_application and sys_sso_provider show no standing term in visible — the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903
Description
Activity
- addedbugSomething isn't workingSomething isn't workingarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSO
on Oct 5, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsTriage: unlocked,
pm:blocked→pm:queue. #21886 landed the standing predicate this closing card applies to the rest of the familyBlocked-by: none
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T08:57Z. ⛔ Not a claim, ⛔ not a dispatch.The blocker is released (read at this write):
- PR fix(platform-objects): offer Add Member only to a platform admin #22064 merged as
77a94d8ced, an ancestor oforigin/main. - [finding]
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 closedcompletedat 2026-10-07T08:35Z.
The predicate, as landed:
sys_member.add_memberreadsvisible: 'current_user.isPlatformAdmin == true'(packages/platform-objects/src/identity/sys-member.object.ts).requiresFeature: 'organization'composes onto it at parse time. That is ruling A-lite (6019378035), and it is the predicate every member here takes.The premise holds on
main:grep -c isPlatformAdminoversys-user.object.ts,sys-oauth-application.object.tsandsys-sso-provider.object.tsprints0for each.For the claimant:
- The member list in the body is the [finding]
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 dev's census, not re-measured. The enumeration pin decides the set: every first-party action whose target door is platform-admin-gated. - On
main,sys_oauth_applicationdeclares five actions, while the census names two. The claimant re-derives each action's door before it is counted. - Reach below platform admin is still measured per action in the first step, as the body says.
- The pin binds
current_userthe way the console does.
The grade stands:
bug·priority:p3·domain:engine·area:identity.- PR fix(platform-objects): offer Add Member only to a platform admin #22064 merged as
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: PM loop round 57 · 2026-10-07T09:31Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21903-platform-admin-affordances
Worktree:objectstack-issue-21903
Domain:domain:engine
Seat:domain:engine#1
Provenance:- Triage filed this card as the family's closing card. It unlocked it to
pm:queue(6034551072) once [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 landed the standing predicate through PR fix(platform-objects): offer Add Member only to a platform admin #22064 (77a94d8ced), under ruling A-lite (6019378035). - It is the only eligible
pm:queuecard in this lane. The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」); finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057's patch round holds one slot.
File surface (atorigin/main56bf27affb), per the card's body and triage's unlock 6034551072: - The members.
packages/platform-objects/src/identity/sys-user.object.ts,sys-oauth-application.object.ts,sys-sso-provider.object.ts, and any other first-party action the census finds.- Each action whose target door is platform-admin-gated (
gateAdmin/judgePlatformAdmin/hasPlatformAdminStandinginplugin-auth) readscurrent_user.isPlatformAdmin == trueinvisible. That is the predicateadd_memberlanded with. - It composes with an existing
visibleand withrequiresFeaturethe waylowerRequiresFeaturecomposes them, with AND.
- Each action whose target door is platform-admin-gated (
- The census decides the set, not the body's list. On
main,sys_oauth_applicationdeclares five actions and the body names two. Each action's door is re-derived fromplugin-auth's mounts before it is counted, and reach below platform admin is measured per action. - The enumeration pin. One test walks every first-party action whose target door is platform-admin-gated and asserts that its served
visiblecarries the predicate. An action added later fails it until it does. Where the pin bindscurrent_user, it binds it the way the console does, throughextra.- If a door-level check is measured necessary, one dogfood case goes under
packages/qa/dogfood/test/(domain:cli, declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 first).
- If a door-level check is measured necessary, one dogfood case goes under
.changeset/21903-*.md(@objectstack/platform-objectspatch).- ⛔ No door,
plugin-auth,packages/specor@objectstack/formulaedit. The ruling did not take any of them.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - Each door and its accept set are unchanged. The card hides affordances from callers each door already refuses with 403. That is a served-metadata courtesy fix at
patch, as [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 shipped. No key, export or parameter is added.
Thread-read: 6034551072
Serial constraints cleared: at 2026-10-07T09:31Z: - Open PRs (fix(metadata-protocol): a row bound to one package is not registered under a name another package ships #22066, feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063, fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041, chore: version packages #21988, feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974), each file list read by
filename: none touchespackages/platform-objects/src/identity/,platform-objects.test.ts,plugin-auth's mounts ororg-admin-affordance-reach.dogfood.test.ts. - This lane's finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057 (PR fix(metadata-protocol): a row bound to one package is not registered under a name another package ships #22066, patch round) touches
metadata-protocolonly. - The family's previous card, [finding]
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886, landed as77a94d8ced.
- Triage filed this card as the family's closing card. It unlocked it to
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21903,
"status": "done",
"branch": "claude/issue-21903-platform-admin-affordances",
"pr": "#22068",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi (subagent; the parent PM's session id, as the order states)",
"premise_still_valid": true,
"summary": "Applied ruling A-lite's predicate to the rest of the family. Thirteen actions now declarevisible: 'current_user.isPlatformAdmin == true', ANDed with their existing terms:\n- sys_user: ban_user, unban_user, unlock_user, create_user, set_user_password, impersonate_user, set_user_manager;\n- sys_oauth_application: disable/enable_oauth_application;\n- sys_sso_provider: register_sso_provider, register_saml_provider, request_domain_verification, verify_domain.\nWhere an action carries requiresFeature, lowerRequiresFeature composes it on at parse time. Pin (a) gives every member a lowering-matrix row. Pin (b) is the new repo-wide enumeration pin, packages/platform-objects/src/platform-admin-affordance-standing.test.ts. It imports every *.object.ts under packages/, picks the actions whose door is platform-admin gated, and evaluates each served predicate with current_user bound throughextra. The census corrected the body on two counts: sys_oauth_application has two members of five, and sys_sso_provider has four of five. The other four actions authorize the session or the record owner, so they are left ungated. Doors, keys, exports and labels are unchanged. Draft PR #22068 is open.",
"hypotheses": {
"H1": "CONFIRMED, with counts corrected. Every first-party action with a /api/v1/auth/ target is in packages/platform-objects/src/identity/. Under the oracle, the family is the 13 above plus sys_member.add_member, which was already gated. sys_oauth_application declares 5 actions on main, and 2 are members. sys_sso_provider declares 5, and 4 are members. Non-members:\n- create_oauth_application: /sys-oauth-application/register, a session-only mount (auth-plugin.ts:3038).\n- rotate_client_secret and delete_oauth_application: vendor @better-auth/oauth-provider 1.7.3 routes that admit the client owner (authorize-9whjxVLJ.mjs:2358, :2456). plugin-auth configures no clientPrivileges.\n- delete_sso_provider: vendor @better-auth/sso 1.7.3 checkProviderAccess, which admits the owner or an org admin of the provider (index.mjs:2250).\n- The datasource type-level actions: a capability door, manage_platform_settings (service-datasource admin-routes.ts:272); :268 rules out an isPlatformAdmin arm.\n- sys_organization.change_slug targets /api/v1/cloud/**, served by another repository. Its door is NOT MEASURED.",
"H2": "CONFIRMED. lowerRequiresFeature (packages/spec/src/kernel/public-auth-features.ts:352) composes(existing) && gateat :417. Where an action already had a visible (set_user_manager and the oauth toggle pair), the standing is ANDed in the repo's existing spelling for an authored composed predicate: one flat conjunction, principal term first. The served results are:\n- ban_user family:(current_user.isPlatformAdmin == true) && features.admin == true;\n- set_user_manager:current_user.isPlatformAdmin == true && has(record.source) && record.source != \"idp_provisioned\";\n- disable_oauth_application:(current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled != true) && features.oidcProvider != false;\n- the four sso actions:current_user.isPlatformAdmin == true.\nEach is pinned in the lowering matrix.",
"H3": "CONFIRMED. No member door admits a standing below platform admin, apart from the legacy role scalar, the same boundary PR #22064 recorded.\n- Offered: I evaluated each served predicate from the built dist with celEngine, binding current_user throughextraas the console does. At 56bf27a every member was offered to all five principals: platform admin, org owner, org admin, delegated admin and plain member. At efd167f each is offered to the platform admin alone, and the four non-members are unchanged.\n- Admitted: the built judgePlatformAdmin on the same session shapes admits the platform admin. It refuses the other four, and a tenant-written platform_admin position without the rung, with 403 PERMISSION_DENIED. It also admits the legacy role=admin scalar. The impersonate caller predicate (admin-impersonate-endpoint.ts:213) refuses the same principals with 403 YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS. Its oracle equals the session rung (auth-manager.ts:4083).\n- Real showcase boot at efd167f: admin-route-nonadmin-refusal and admin-platform-admin-standing passed 14 of 14, covering all 13 member doors in both directions.\n- sys_sso_provider's list also requires manage_platform_settings (sys-sso-provider.object.ts:60). That is a capability, not the rung, so the predicate is still needed.",
"H4": "CONFIRMED: no structural oracle exists without a new export.\n- plugin-auth exports no mount table.\n- auth-route-ledger.ts is package-internal, and its rows state the gate only in note prose.\n- VENDOR_ADMIN_PATH_PREFIX names the namespace, but plugin-auth is not a dependency of platform-objects.\nSo the pin uses the narrowest honest form, read off the mounts: the /api/v1/auth/admin/ namespace, minus /admin/has-permission (a query that answers every caller) and /admin/stop-impersonating (it admits the impersonated session), plus /api/v1/auth/organization/add-member. Its header names what it misses: a gated mount added outside the namespace, a capability-gated door, routes another repository serves, and actions not declared on an object file. Dogfood pin (c) was not needed: the door level is already pinned by the two dogfood sweeps above."
},
"census": [
"action | target | door gate (file:line) | gated | visible before → after (served)",
"sys_user.ban_user | /admin/ban-user | auth-plugin.ts:2670 gateAdmin :2672 | yes | features.admin == true → (current_user.isPlatformAdmin == true) && features.admin == true",
"sys_user.unban_user | /admin/unban-user | auth-plugin.ts:2691 gateAdmin :2693 | yes | same as ban_user",
"sys_user.unlock_user | /admin/unlock-user | auth-plugin.ts:2490 judgePlatformAdmin :2502 | yes | same as ban_user",
"sys_user.create_user | /admin/create-user | auth-plugin.ts:2601 gateAdmin :2603 | yes | same as ban_user",
"sys_user.set_user_password | /admin/set-user-password | auth-plugin.ts:2851 gateAdmin :2853 | yes | same as ban_user",
"sys_user.impersonate_user | /admin/impersonate-user | admin-impersonate-endpoint.ts:198, predicate :213, wired auth-manager.ts:3609 | yes | same as ban_user",
"sys_user.set_user_manager | /admin/set-user-manager | auth-plugin.ts:2536 gateAdmin :2538 | yes | has(record.source) && record.source != "idp_provisioned" → current_user.isPlatformAdmin == true && has(record.source) && record.source != "idp_provisioned"",
"sys_oauth_application.disable_oauth_application | /admin/oauth2/toggle-disabled | auth-plugin.ts:2355 judgePlatformAdmin :2372 | yes | (has(record.disabled) && record.disabled != true) && features.oidcProvider != false → (current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled != true) && features.oidcProvider != false",
"sys_oauth_application.enable_oauth_application | /admin/oauth2/toggle-disabled | as above | yes | same shape with record.disabled == true",
"sys_sso_provider.register_sso_provider | /admin/sso/register | auth-plugin.ts:2468 gateAdmin :2470 | yes | none → current_user.isPlatformAdmin == true",
"sys_sso_provider.register_saml_provider | /admin/sso/register-saml | auth-plugin.ts:2958 gateAdmin :2962 | yes | none → current_user.isPlatformAdmin == true",
"sys_sso_provider.request_domain_verification | /admin/sso/request-domain-verification | auth-plugin.ts:2983 gateAdmin :2988 | yes | none → current_user.isPlatformAdmin == true",
"sys_sso_provider.verify_domain | /admin/sso/verify-domain | auth-plugin.ts:3002 gateAdmin :3005 | yes | none → current_user.isPlatformAdmin == true",
"sys_member.add_member (precedent) | /organization/add-member | auth-plugin.ts:2932 gateAdmin :2934 | yes | unchanged: (current_user.isPlatformAdmin == true) && features.organization != false",
"sys_oauth_application.create_oauth_application | /sys-oauth-application/register | auth-plugin.ts:3038, session only | no | unchanged: features.oidcProvider != false",
"sys_oauth_application.rotate_client_secret | /oauth2/client/rotate-secret | vendor owner check (oauth-provider 1.7.3 authorize-9whjxVLJ.mjs:2456) | no | unchanged: features.oidcProvider != false",
"sys_oauth_application.delete_oauth_application | /oauth2/delete-client | vendor owner check (authorize-9whjxVLJ.mjs:2358) | no | unchanged: features.oidcProvider != false",
"sys_sso_provider.delete_sso_provider | /sso/delete-provider | vendor checkProviderAccess (sso 1.7.3 index.mjs:2250) | no | unchanged: none"
],
"clause_2": "no, measured against BASE 56bf27a:\n- 0 changed files outside packages/platform-objects and .changeset;\n- 0 addedexportlines in non-test source;\n- the only non-comment source lines that change are values of the existing authorablevisiblekey on existing actions (13 added, 3 replaced);\n- 0 plugin-auth, packages/spec or @objectstack/formula files are touched.\nEach door and its accept set are unchanged. H3 reads the built gate, and two dogfood sweeps on a real boot pass 14 of 14. The line rides the changeset and the PR body.",
"files_changed": [
".changeset/21903-platform-admin-affordance-visibility.md (new; @objectstack/platform-objects patch, Clause-②: no)",
"packages/platform-objects/src/identity/sys-user.object.ts (standing on 7 actions plus comments)",
"packages/platform-objects/src/identity/sys-oauth-application.object.ts (standing on the toggle pair plus comment)",
"packages/platform-objects/src/identity/sys-sso-provider.object.ts (standing on the 4 bridge actions plus comments)",
"packages/platform-objects/src/platform-objects.test.ts (pin a: matrix rows for every member, toggle exact-source pins)",
"packages/platform-objects/src/platform-admin-affordance-standing.test.ts (new; pin b, the enumeration pin)",
"packages/platform-objects/src/identity/action-predicate-sparse-face.test.ts (fixture re-judged: principal bound through extra with the rung)",
"packages/platform-objects/src/identity/sys-user-set-manager-action.test.ts (fixture re-judged: platform admin bound through extra)"
],
"line_budget": "n/a",
"tests": "All runs at final HEAD efd167f. Builds and tests went through os-verify-lock.sh; each VERDICT line was read.\n- The four touched test files: 4 files, 154 tests passed.\n- Full package,pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 63 files, 1006 tests passed. On main it ran 62 / 996; this adds 5 pin cases and 5 matrix rows.\n- Pin (b) alone, verbose: 5 of 5 passed.\n-pnpm --filter @objectstack/platform-objects typecheckpassed. The test-layer program compiles the four touched tests (--listFiles: 1 hit each, 0 in the build program). Its only errors are the 3 ledgered in src/feature-gate-guard.test.ts.\n- Dogfood, real showcase boot: admin-route-nonadmin-refusal plus admin-platform-admin-standing passed 14 of 14, and org-admin-affordance-reach passed 14 of 14.\nReverse verification on committed HEAD efd167f. Prediction: red.\n- Mutation: scripts/ablation-replace.mjs replaced set_user_manager's predicate with its pre-change text. The anchor went 1 → 0 and the blob 0b9e8e738fb4 → 0ac51042a82d, under an outer EXIT/INT/TERM trap with absolute paths.\n- No dist/ is in the resolution path: both pins import the subject from source.\n- Result: red, 3 failed of 137. (a) The SysUser.set_user_manager matrix row expected the composed predicate and received the mutated one. (b) "carries the standing term" listed set_user_manager, and "no refused principal is offered" listed 5 leaks: owner, admin, delegated, member, and the tenant-written position.\n- Restore:git checkout HEAD --on the absolute path. The blob after the restore equals the HEAD blob 0b9e8e738fb4911b9a379b2ebc699344f68127bd,git diff HEADis empty and the status is clean.",
"gates": "At efd167f. origin/main was 56bf27a, equal to BASE and an ancestor of HEAD (is-ancestor exit 0), so there was nothing to merge.\n- Full build: turbo run build --filter=!@objectstack/docs --concurrency=2 passed 72 of 72 tasks.\n- Derived gates: dispatch-gates --commands --repo objectstack-ai/objectstack derives 63 (31 pnpm, 32 node), the same list as the order. All 63 exited 0.--ranwith exit codes: "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.\n- Artifact-roster block: 54 commands, 51 exit 0. Three answered NOT WIRED (exit 2) because they need PR context: check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths. check-partof-closing-keyword was rerun with the PR body as PR_BODY and passed. The other two need the PR number, and CI runs them.\n- NOT MEASURED: check-sdui-manifest's objectui version comparison (no objectui checkout). check:console-injection ran its self-test only (no console dist).\n- The four symbol-anchor sweeps (check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors) all passed.\n- Lint, as a proven narrowing: eslint --no-inline-config --format json over the 7 touched TS files counts 7 files, 0 errors, 0 warnings. --print-config resolves each file. Type-aware linting is off: no parserOptions.project or projectService, and eslint.config.mjs:327-328 states it.\n- Left to CI, declared: the type-check lanes, Test Core, the rest of the Dogfood Regression Gate, Build Core, Temporal Conformance, and the repo-wide pnpm lint.",
"deviations": [
"Twogit pushstrokes, not one: the empty-branch write probe that os-dev.md rule 1 requires, then the single commit. Both are git pushes, not REST writes.",
"Two existing tests in the declared identity/ directory were re-judged; the claim's file list did not name them. action-predicate-sparse-face.test.ts and sys-user-set-manager-action.test.ts bound a principal without the rung throughuser:. With the standing in place, the sweep's record half would short-circuit and two verdicts would go red. Both now bind throughextra, as the console does.",
"Commit trailers are the model-free pair AGENTS.md prescribes. The harness reminder asked for a model-named Co-Authored-By trailer, and AGENTS.md takes precedence over it.",
"The PR carries four labels: documentation, size/m, tests and tooling. I did not write them (path labelers). My label-write touched the assignee only.",
"My first batch of build-independent gates hit my own 590 s timeout while it shared CPU with the full build. It had finished 49 of 56, all exit 0. I resumed from command 50, and every command has a recorded exit code.",
"Cleanup: worktree node_modules was removed before this comment.git worktree remove(without --force) runs right after it; the tree is clean, and HEAD efd167f equals the remote branch."
],
"mcp_calls": "0",
"api_writes": "3 relay strokes, each aPOST /repos/objectstack-ai/objectstack/dispatchesfrom the session, executed as objectstack-fleet[bot]:\n1. pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft) → #22068. Read back: 17815 bytes, identical.\n2. label-write assign, POST /repos//issues/22068/assignees [os-project-manager]. Read back: it matches.\n3. This os-dev-report comment, POST /repos//issues/21903/comments.\nPlus the git pushes above, which are not REST.",
"open_questions": [],
"out_of_scope_findings": [
"observation (not class a/b/c; read-only inference, not reproduced): request_domain_verification and verify_domain pass the platform-admin gate, then delegate to @better-auth/sso checkProviderAccess (1.7.3 index.mjs:2250). So does delete_sso_provider's door, with no gate in front. That check admits only the provider owner or an org admin of the provider's organization, so by reading, a second platform admin would be refused on an org-less provider they did not register. reach: NOT MEASURED · dedupe words: checkProviderAccess, sso provider owner, second platform admin, delete-provider · carrier: 承接者:无 · noted in the PR Acceptance notes, not filed",
"observation (not class a/b/c; ruled predicate): every member door also admits the legacy better-authuser.role === 'admin'scalar (plugin-auth platform-admin-gate.ts:80-84, and the impersonate predicate's first half). The standing predicate does not read it. This is the boundary PR #22064 recorded, and nothing writes that scalar for a platform admin. carrier: 承接者:无 · noted, not filed",
"NOT MEASURED: sys_organization.change_slug targets /api/v1/cloud/organizations/{id}/change-slug, served by objectstack-ai/cloud. Its door was not classified, and the pin's oracle does not cover that prefix. carrier: 承接者:无 · noted, not filed"
]
}objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsLanded: PR #22068 →
879bd38c5bonmain. It merged through the merge queue at 2026-10-07T11:24Z, after entering the queue at 2026-10-07T10:56Z. Verified at 2026-10-07T11:24Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash. It is on
origin/mainas a single-parent commit (parentae97841556). Its diffstat is the reviewed one: 8 files, +442/-18. - What is on
main(ruling A-lite's predicate, 6019378035). Thirteen actions whose door is platform-admin-gated now carrycurrent_user.isPlatformAdmin == trueinvisible, composed with their existing terms andrequiresFeature:sys_user:ban_user,unban_user,unlock_user,create_user,set_user_password,impersonate_userandset_user_manager;sys_oauth_application:disable_oauth_applicationandenable_oauth_application;sys_sso_provider:register_sso_provider,register_saml_provider,request_domain_verificationandverify_domain.- The census corrected the body: the other four actions on those objects authorize the session or the record's owner, so they stay ungated.
- The enumeration pin
platform-admin-affordance-standing.test.tswalks every first-party action whose target is a platform-admin-gated mount (the/api/v1/auth/admin/namespace minus two caller-open mounts, plusadd_member's). It fails one added later without the standing term, and its header names what it does not cover. - The card.
Fixes #21903closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body. - From this release (
@objectstack/platform-objectspatch,Clause-②: no): a member, org owner, org admin or delegated admin no longer sees the 13 actions their doors refuse with 403. A platform administrator still does. - The family is closed: console: workspace members page offers Invite / Remove member to the
memberrole; only the server 403 stops it #8092 (workspace members,not_planned), platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795 (the grade-gated set), [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (add_member) and this card. - The seat's ACCEPT is on PR fix(platform-objects): offer the platform-admin-gated user, OAuth and SSO actions only to a platform admin #22068 (6036433387). Three notes ride the PR's Acceptance notes with
承接者:无:@better-auth/sso's owner check on a second platform admin (read, not measured), the legacyrole === 'admin'scalar, andsys_organization.change_slug's door in another repository.
Generated by Claude Code
- The squash. It is on
- added 3 commits that reference this issue
on Oct 7, 2026
Blocked-by: #21886
Filed by the triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U, answering thedomain:engineseat's question 3 on #21886 (5999894453). ⛔ Not a claim, ⛔ not a dispatch.Why a closing card
"An affordance offered that its door refuses" has now occurred three times: #8092 (workspace members), #21795 (the grade-gated org set) and #21886 (
add_member). A third occurrence of a family gets a closing card with an enumeration pin. So the remaining members are carried here, not as one card each.The members (the #21886 dev's census,
5999863859, not re-measured)sys_user:ban_user,unban_user,unlock_user,create_user,set_user_password,impersonate_user,set_user_manager;sys_oauth_application: two actions;sys_sso_provider: four actions.Each targets a platform-admin-gated door (
/api/v1/auth/admin/*or the SSO mounts) and carries no standing term invisible. Reach below platform admin is NOT MEASURED. The first step measures it per action.The step (after #21886's ruling)
visiblereads the standing predicate the ruling on [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 names, the same oneadd_membergets.visiblecarries that predicate. An action added later fails the pin until it does. The pin bindscurrent_userthe way the console does, not throughuser:, as the [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 dev noted for the existing dogfood file.Grade
bug·priority:p3(it fails closed: each door refuses with 403) ·domain:engine(platform-objects) ·area:identity·pm:blockedon #21886.Generated by Claude Code