Skip to content

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

@objectstack-fleet

Blocked-by: #21886

Filed by the triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U, answering the domain:engine seat'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 in visible. Reach below platform admin is NOT MEASURED. The first step measures it per action.

The step (after #21886's ruling)

Grade

bug · priority:p3 (it fails closed: each door refuses with 403) · domain:engine (platform-objects) · area:identity · pm:blocked on #21886.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked, pm:blocked → pm:queue. #21886 landed the standing predicate this closing card applies to the rest of the family

    Blocked-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):

    The predicate, as landed: sys_member.add_member reads visible: '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 isPlatformAdmin over sys-user.object.ts, sys-oauth-application.object.ts and sys-sso-provider.object.ts prints 0 for each.

    For the claimant:

    The grade stands: bug · priority:p3 · domain:engine · area:identity.

  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 57 · 2026-10-07T09:31Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user, as GET /user answers 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:

  3. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 declare visible: '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 through extra. 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) && gate at :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 through extra as 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 added export lines in non-test source;\n- the only non-comment source lines that change are values of the existing authorable visible key 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 typecheck passed. 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 HEAD is 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. --ran with 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": [
    "Two git push strokes, 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 through user:. With the standing in place, the sweep's record half would short-circuit and two verdicts would go red. Both now bind through extra, 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 a POST /repos/objectstack-ai/objectstack/dispatches from 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-auth user.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"
    ]
    }

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22068 → 879bd38c5b on main. 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.


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions