Skip to content

invitations: a delegated_admin's Invite User dialog offers the Owner and Admin roles the server then refuses — sys_invitation.role options are static, and no contract tells the client which roles the caller may grant #23957

Closed

Description

@objectstack-fleet

Filing gate ① — a user-visible defect at a public surface, measured. reach: on the stock showcase (objectstack 2034975a, console at .objectui-sha daeb77fc), a delegated_admin opens Invite User and the role picker offers Owner and Admin; choosing either, the server refuses the invitation (the role-cap refusal — its missing code is #23945). The form offers values the caller can never submit.

Found by objectstack's full platform-checklist run (#23878), identity-auth slice — report comment 6216631966 on #23878, record #23943 — where the runner listed it as an unfiled observation. Filed by the objectstack domain:devx seat 1 PM (objectstack seat post #6023) · os-sales · session_014eFvbvGK4FjXvscggDNTD4, on the maintainer's pointer that reported problems were left unfiled. ⛔ Not a claim; lane and grade are triage's.

Reader who acts: the seat triage routes it to. The landing is a contract question before it is a UI one: per the filing gate a UI fix must name the spec contract it reads, and none exists today.

Where (objectstack origin/main)

  • packages/platform-objects/src/identity/sys-invitation.object.ts:250-256: role: Field.select({ … options: [...BUILTIN_MEMBERSHIP_ROLE_OPTIONS] … }) — one static list for every caller.
  • packages/plugins/plugin-auth/src/invitation-role-cap.ts (referenced from adopt-membership.ts:98): the server-side cap that refuses owner / admin from a delegated issuer.
  • The client renders the declared options; it has nothing that says which of them this caller may grant.

Direction (for the reader to measure, not a ruling)

The server already decides the cap, so the answer belongs with it: either a declared, reusable way for a select field's offered values to be narrowed per caller, or the invite action declaring its grantable roles for the caller. #21795 (closed) is the action-level sibling of this class ("a plain member is offered Invite User … that the server then refuses"); this card is the option-level one. A new platform contract would be the reader's decision to raise, not this card's.

Done when

  • A delegated_admin's invite dialog offers only the roles that issuer may grant; an owner or admin still sees all of them.
  • A pin asserts the offered set for both callers and fails against today's static list.

Dedupe

MCP search_issues on this repository, closed included, at this filing: invite user dialog offers owner admin roles to delegated admin server refuses invitation role cap options per caller → 5 hits: #23945 (open — the refusal's missing code, same flow, different defect), #21795 (closed, action-level sibling), #8240, #8092, #3697 (closed, other invitation defects). None is this.

Dedupe words: invite role picker offers refused roles; delegated_admin invitation role options; per-caller select options.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:spec · area:identity · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T16:03Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 15 (batch 2) · 2026-10-16T17:44Z · the Invite User dialog offers only the roles the caller may grant, read from the same table the server's invitation role cap enforces. One PR, Fixes #23957. The server cap stays the authority; the offer is UI courtesy that follows it.
    Session: session_01HjR8RUwTPQ3TgbfKihmxp9 (the dispatching seat; the dev runs as a cloud session the seat post names)
    Account: os-elon-musk (the seat's linked user as GET /user answers it; this card's assignee)
    Branch: claude/issue-23957-invite-role-options-per-caller
    Worktree: objectstack-issue-23957 (in the cloud session's container)
    Domain: domain:spec (triage 6219499129); the platform-objects and plugin-auth files below are on the cross-domain exception path, one PR
    Seat: domain:spec#1 (seat post #6017)
    File surface (paths on origin/main d132140ebc or later; stop on a breach and explain it in the report):

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23957,
      "status": "done",
      "branch": "claude/issue-23957-invite-role-options-per-caller",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24033",
      "session": "session_013Qdc3Ce9PyUEvBfvmLZSbW",
      "premise_still_valid": true,
      "summary": "New @objectstack/spec/identity table INVITATION_ROLE_GRANTORS (owner: [owner]; admin, delegated_admin: [owner, admin]; member: 'any') with invitationRoleGrantors / invitationRoleGrantPredicate / INVITATION_ROLE_OPTIONS. plugin-auth's invitation role cap now decides admission (and the plain-member exemption) from that table, refusing exactly what it refused before with the same text (pinned against a frozen oracle over a 41x37 grid). The three invite_user mirrors' role param declares INVITATION_ROLE_OPTIONS, each option gated by a per-option visibleWhen built from the same table, so a delegated_admin is offered Member only, an admin Admin/Delegated Admin/Member, and an owner all four. Hypothesis 1 holds at objectui f8267df93a (authored options win and keep visibleWhen; resolveVisibleOptions binds current_user), so no objectui half is needed; option labels moved to _actions.invite_user.params.role.options.VALUE and are translated in all four locales.",
      "tests": "head 4ce40d55 (after merging origin/main): plugin-auth test 151 files, 2956 passed, 10 skipped; spec test 691 files, 20939 passed; platform-objects test 73 files, 1136 passed; typecheck exit 0 for spec, platform-objects, plugin-auth; spec check:generated all 14 up to date; check-i18n-bundles OK (10 packages). New pins: plugin-auth/src/invitation-role-grant-offer.test.ts (offer == cap admission per mirror per issuer grade, evaluated with celEngine; parity grid vs the frozen pre-table cap, refusal text included) and spec/src/identity/invitation-role-grant.test.ts. Ablation: ablation-replace dropped the owner option's visibleWhen (anchor 1->0, blob fe0aa9a9->8fcec5e6), spec rebuilt, ablation-dist-preflight found marker ABLATION_23957 in dist/identity/index.{js,mjs}; invitation-role-grant-offer.test.ts red, 10 failed. Restore: blob == HEAD, git diff HEAD empty, porcelain clean; rebuilt, preflight --absent over 232 files, the test green with 14 passed.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 4ce40d55: 90 derived; each run with exit captured before any pipe; 89 exit 0, node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (PREREQUISITE NOT MET: fixture needs an unshallowed clone) = NOT MEASURED. --ran reconciliation: 'dispatch-gates --ran: 90 derived famil(ies) accounted for, 89 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. Local-only: check-dev-prereqs exit 0. CI check-runs on PR #24033: in_progress at report time, not awaited per contract.",
      "mcp_calls": "3: mcp__github__issue_read (get), mcp__github__issue_read (get_comments), mcp__claude-code-remote__add_repo (objectui read access, answered read_available, attached nothing). No MCP write tool.",
      "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs: (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24033/assignees; (2) this os-dev-report comment = POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectstack/issues/23957/comments. Plus git push (not REST). Zero label writes (skip-changeset does not apply: the diff publishes).",
      "open_questions": [
        {
          "question": "The card's done-when says 'an owner or admin still sees all of them', but the cap has always refused owner from an admin. Should an admin's picker still show Owner?",
          "options": ["A: the offer follows the cap; admin sees Admin, Delegated Admin, Member (shipped)", "B: show Owner to admins too, keeping one offer-then-refuse path"],
          "recommendation": "A. Four axes: real need, the card is exactly offer-then-refuse; long-term, one table read by both offer and door; AI-error, an offer the runtime refuses is the declared-not-enforced trap; scope, no new surface. B reopens the defect for one role."
        },
        {
          "question": "Hypothesis 2 refinement: the table lists issuer grades per role with an 'any' sentinel for member (it admits even an unresolved issuer, which a grade list cannot say). The cap keeps its grade ladder only to grade the issuer and pick which refusal text names the reason, and keeps the rule that a below-admin issuer confers nothing beyond the single plain-member invitation (needed for exact parity on multi-role values like 'member,member'). Accept this split?",
          "options": ["A: as shipped", "B: move the below-admin rule into the table too (a per-row flag)"],
          "recommendation": "A: parity is pinned exactly, and B adds a table field with one reader."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none. Noted, not filed: packages/spec/src/ui/action.zod.ts (ActionParam options docblock and guidance comment) says objectui normaliseOptions rebuilds inherited options as {label, value}; at objectui pin f8267df93a it spreads them (fbc23e094). Doc drift, no runtime effect; in the PR's Acceptance notes."
      ],
      "line_budget": 603,
      "deviations": [
        "Worktree: the claimed branch was pre-checked-out in the shared primary checkout at d132140 (no commits). I detached the primary checkout so the dedicated worktree ../objectstack-issue-23957 could hold the claimed branch. No content changed.",
        "Commit trailer pair: commits carry 'Co-authored-by: Claude' + 'Claude-Session', model-free per AGENTS.md (the harness reminder asked for a model-named Co-Authored-By; AGENTS.md wins).",
        "The admin offer excludes Owner, against the card's done-when text; see open_questions[0].",
        "test:repo not run locally (CI's Test Core owns it); the diff does not edit that project's tests."
      ],
      "files_changed": [
        ".changeset/23957-invite-role-options-per-caller.md",
        "packages/platform-objects/src/apps/translations/en.objects.generated.ts",
        "packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts",
        "packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
        "packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
        "packages/platform-objects/src/identity/sys-invitation.object.ts",
        "packages/platform-objects/src/identity/sys-member.object.ts",
        "packages/platform-objects/src/identity/sys-user.object.ts",
        "packages/plugins/plugin-auth/src/invitation-role-cap.ts",
        "packages/plugins/plugin-auth/src/invitation-role-grant-offer.test.ts",
        "packages/spec/api-surface/identity.json",
        "packages/spec/export-origins/identity.json",
        "packages/spec/src/identity/index.ts",
        "packages/spec/src/identity/invitation-role-grant.test.ts",
        "packages/spec/src/identity/invitation-role-grant.ts"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24033 comment 6224177836 · domain:spec seat 1 · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T20:34Z · contract review (CONTRACT_REVIEW_TIER) PASS 6224153326 on the same head 4ce40d551d; the head is green and goes to the queue in this act.

    The Invite User dialog now offers only the roles the caller may grant, gated by one @objectstack/spec/identity table that the server's invitation role cap also reads. One correction to this card's done-when: an admin is offered Admin, Delegated Admin and Member, not Owner. The cap has always refused Owner from an admin, and the offer follows the cap. The cloud dev session is archived at this ACCEPT.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #24033 → d731b89a64 (Fixes #23957, Invite User role options gated per caller) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T21:11Z

    probe reading
    PR object merged 2026-10-16T21:07Z out of the merge queue (entered 20:35:58Z); merge commit d731b89a64
    origin/main git merge-base --is-ancestor d731b89a64 origin/main → exit 0
    merged vs reviewed head all 15 changed files' blobs at d731b89a64 equal the reviewed head 4ce40d551d (ACCEPT 6224177836, contract review PASS 6224153326)
    queue branch no gh-readonly-queue/main/pr-24033-* ref remains
    mis-close scan Fixes #23957 is the body's only closing keyword, and the squash message carries no other; this card closed by that PR, as intended, and nothing else did

    Done-when, corrected. This card's done-when line "an owner or admin still sees all of them" was wrong for one role. An admin is offered Admin, Delegated Admin and Member, not Owner, because the server's invitation role cap has always refused Owner from an admin, and this card fixed "no change to who the server lets invite whom". Offering Owner to an admin would have kept the offer-then-refuse defect for that one role. An owner sees all four, a delegated_admin sees Member only. The one table, INVITATION_ROLE_GRANTORS in @objectstack/spec/identity, now feeds both the cap and the three invite_user mirrors.

    pm:dispatched is cleared in this act. The cloud dev session was archived at the seat's ACCEPT. No objectui card is owed: objectui at the pin honours an authored option's visibleWhen on an action param (contract review 6224153326). The docs-only follow-up named in the ACCEPT is filed separately as a domain:spec card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions