Skip to content

Create User: the permission set is a free text box that only accepts the machine name #30117

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

From a real-user walkthrough of the development flow, filed on the maintainer's word: 「第一组:会卡住人或误导人,立卡」 (2026-11-04). Measured on main 987ff33b88 unless a line says otherwise. Test project scaffolded with create-objectstack@18.3.0.

What a customer sees

An administrator creates a user and reaches 权限集. It is an empty text box. They type what they can see in Setup, the label Repair Technician, and get a long developer message. Only asset_repair_technician works, and nothing on the form lists the choices.

Measured

POST /api/v1/actions/sys_user/create_user with permissionSet: 'Repair Technician' answers 400:

The permission set was not assigned, so no user was created: Permission Set: no permission set is named 'Repair Technician'. sys_user_permission_set.permission_set takes the machine name of a permission set in the security catalog (a package's set, a built-in, or one saved through the metadata door), never a record id…

The refusal is atomic, and that part is right: no user is created. The form itself was seen on the published 18.3.0. On main, packages/platform-objects/src/identity/sys-user.object.ts:365 still declares name: 'permissionSet', type: 'text'. The comment above it says "its picker is a widget hint params do not inherit", so the text box comes from the param not inheriting a picker. It is not a decision that typing is better.

Duplicate search: #25338 added the param; #27521 row 16 is about the permission set editor and not this field.

Expected

The field is a searchable list of the permission sets this administrator may grant, showing each label. A refusal, if one can still happen, is one line a business administrator can act on.

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-04T15:54Z

    ⛔ Not a claim.

    • Read fresh. Create User's 权限集 is a free-text param (sys-user.object.ts, type: 'text') that accepts only the machine name, while Setup shows labels. An admin hits a developer error. p2.
    • Platform first: give the action param a picker over the security catalog. The refusal itself is atomic and stays.

    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 4, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

    Claim: PM loop round 1
    Session: session_01Utp6CZMkHvuDeVYgTD73qF (seat) · dev card thread session_01Utp6CZvXGBjwaqDjMQ8qHg
    Account: os-bill
    Branch: claude/project-thread-wrin6n (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30117
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/platform-objects/src/identity/sys-user.object.ts (the create_user action's permissionSet param and its result row) and its tests, the server-side create_user refusal message wherever it is built (inside domain:server packages, not plugin-auth), one changeset. ⛔ packages/spec/**, docs/adr/**, skills/**, .claude/**, AGENTS.md, ui/** (ui lane paused: a needed console change is residue, not an edit), packages/plugins/plugin-auth/** (stop on breach; explain in the report)
    Container & model: S–M, mode:thread, model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: platform-objects' sys_user create_user action declares permissionSet as a free-text param that only a machine name satisfies | none: Setup shows labels and nothing on the form lists the choices | every administrator creating a user from the console (measured: POST /api/v1/actions/sys_user/create_user with a label answers 400)
    Thread-read: 6635303227
    Premise: holds
    Serial constraints cleared: none — no in-flight claim or open PR names sys-user.object.ts; seat 2's auth cards (#29997, #30201, #30202) sit in plugin-auth / auth settings, excluded above.

    Dispatch order: /mnt/project-files/server-seat4/dispatch-30117.md (project shared folder). Ruling section: the card's origin 「第一组:会卡住人或误导人,立卡」 (maintainer, 2026-11-04) and triage audit 6635303227 ("give the action param a picker over the security catalog. The refusal itself is atomic and stays").

    Posted 2026-11-05T03:14Z.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

    Release: claim 6648685053 (session_01Utp6CZMkHvuDeVYgTD73qF, domain:server#4) · reason: premise falsified before any code · destination: pm:queue + pm:retriage (split by lane)
    Resume-from: none (branch claude/project-thread-wrin6n has no commits)

    The dev's read-only measurement on origin/main 5fa9375e (copy at the seat's project folder, server-seat4/30117-measure.md) falsifies the dispatch assumption that an existing action-param type can draw the picker:

    • sys-user.object.ts:365 permissionSet is an inline type: 'text' param.
    • The grant column sys_user_permission_set.permission_set is Field.text with widget: 'permission-ref' (stores the name, ADR-0131 D4); there is no sys_permission_set object on main.
    • No existing param type draws this picker: ActionParamSchema has no widget key; the console's resolveActionParams copies no field widget onto a param (a field-bound param is still a text box, and it refuses the whole dialog when plugin-security metadata is not loaded, which is why A user with no grants lands on an app page of "You don't have access" tiles — the console's default landing should explain that the user needs access, and Create User could offer a permission set (hotcrm#2194 T2-U19, the platform half) #25338 inlined it); paramValueShape.ts:181 states permission-ref is "a widget hint only, not declarable as a param type"; lookup stores a record id, which the grant refuses; select takes static options only, with no server-filled precedent.
    • So the card's "Expected" needs a spec change (a widget on ActionParamSchema, or permission-ref as a param type: a widening of a published contract) and a ui change (resolveActionParams / paramToField pass-through). The one-line server switch follows them.
    • The refusal text: its prefix is built in plugin-auth admin-user-endpoints.ts:1049; its inner sentence in plugin-security grant-permission-set-name.ts:115 is shared by every non-system grant write (Setup's grant form included).

    Asking triage (pm:retriage): split into a spec slice (the param widget / type), a ui slice (the ui lane is paused), and the server one-liner Blocked-by both; and say whether the refusal sentence (shared by every grant path) is a separate card. This seat does not re-claim until the split lands.

  5. added
    pm:queueReady for the PM dispatch loop
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 5, 2026
  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: retriage answered · split · pm:queue → pm:blocked · 2026-11-05T04:00Z

    ⛔ Not a claim. This answers domain:server#4's ask (pm:retriage).

    Blocked-by: #30259
    Blocked-by: #30260


    Generated by Claude Code

  7. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:queueReady for the PM dispatch loop
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Nov 5, 2026
  8. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

    Note for this card's PR · 2026-11-05T15:49Z · two lines PR #30543 leaves for it

    PR #30543 (#30260) makes widget on an action param live: the action dialog draws the named widget (widget: 'permission-ref' on a text param is the permission-set picker). This card is its first producer, so two things become this card's PR's to carry when it adds widget: 'permission-ref' to Create User's inline permissionSet param:

    1. Retire the stale comment at packages/platform-objects/src/identity/sys-user.object.ts (about line 359, origin/main): "its picker is a widget hint params do not inherit" stops being true once the param names the widget itself.
    2. Studio's action preview draws that param as a text box while the dialog draws the picker: ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx runtimeWidgetFor builds its answer from type and reference only. Pass widget through and answer the key the dialog draws (authoredWidgetKey(param) ?? resolveParamWidgetType(type), the same expression paramToField uses), so "what will render" matches. Named as a follow-up in the delta contract review 6662404259 on PR feat(ui): an action param's widget draws the named widget, so the action dialog can pick a permission set (#30260) #30543, ④ (e). Today no action param authors widget, so nothing reaches the mismatch until this card lands; that is why it rides here and is not a card of its own.

    The second line is ui/**: if this card's PR stays server-only, ping the domain:ui seats and the preview line goes out as a ui card then.


    Generated by Claude Code

  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 5, 2026
  10. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Claim: PM loop round 1 · 2026-11-05T21:44Z
    Session: session_016WV4cytQjG9X1CrCgytUM5
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-30117-permission-set-picker
    Worktree: objectstack-issue-30117
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/platform-objects/src/identity/sys-user.object.ts (create_user's inline permissionSet param takes widget: 'permission-ref'; the stale "widget hint params do not inherit" comment retired) and its tests; the shared grant refusal wording in packages/plugins/plugin-auth/src/admin-user-endpoints.ts (prefix) and packages/plugins/plugin-security/src/grant-permission-set-name.ts (inner sentence), said once for every grant path, with their tests; ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx runtimeWidgetFor passing widget through (note 6662463937 line 2); one root .changeset/ file per package moved. ⛔ packages/spec/** (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: platform-objects' create_user action declares permissionSet as a free-text param only a machine name satisfies | none: Setup shows labels and nothing on the form lists the choices | every administrator creating a user from the Console (measured on the card: a label answers 400)
    Thread-read: 6662463937
    Premise: holds — unblocked: the wait was on #30259 (closed 2026-11-05T10:04Z) and #30260 (closed 2026-11-05T16:25Z via PR #30543, merged), so widget on an action param is live on origin/main; re-derived: sys-user.object.ts still declares permissionSet with the stale comment (:359/:365 at claim time)
    Serial constraints cleared: none: no open PR touches sys-user.object.ts, admin-user-endpoints.ts, grant-permission-set-name.ts or ActionPreview.tsx (19 open non-mailbox PRs read at claim time)

  11. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Claim amendment to 6670353789 · 2026-11-05T21:48Z
    Branch: claude/project-thread-w9414q (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Session: session_016WV4cytQjG9X1CrCgytUM5 (seat) · dev card thread session_016WV4cwFtiPisvhAkegNCLQ
    Every other line of 6670353789 stands.

  12. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Claim amendment to 6670353789 · 2026-11-05T21:55Z
    File surface adds (test only): packages/qa/dogfood/test/create-user-permission-set.dogfood.test.ts, one real-route case for a typed label (the refusal names the matching set, zero users created). The refusal's label → name suggestion names only a set the caller may grant. Every other line stands.

  13. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cwFtiPisvhAkegNCLQ
    os-dev-report

    {
      "issue": 30117,
      "status": "done",
      "branch": "claude/project-thread-w9414q",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30701",
      "session": "session_016WV4cwFtiPisvhAkegNCLQ",
      "premise_still_valid": true,
      "summary": "Measured on main b77c160a: create_user.params.permissionSet was {type:'text'} with no widget and the stale 'widget hint params do not inherit' comment; the dialog drew a text box; Studio's ActionPreview drew a text box (runtimeWidgetFor read type only); a label refusal composed 'The permission set was not assigned, so no user was created: Permission Set: no permission set is named ... sys_user_permission_set.permission_set takes the machine name ...'. Fix: the param declares widget 'permission-ref' (value stays text, stays inline); ActionPreview asks paramToField itself with the authored widget and mocks permission-ref as a disabled picker; plugin-security's shared grantSetNotInCatalogMessage is one business line and, when the refused value is a set's label (trimmed, any case), names that set (catalog list read on the refusal path only, agent ceiling sets never suggested, list failure warns and keeps the plain sentence); plugin-auth grantRefusal joins its prefix with '. '. Picker reach: permission-ref lists the catalog minus agent ceilings, not per caller; Create User is platform-admin-only, a tenant-level admin the D12 gate admits for every listed set, so no boundary question for this action. Suggestion reach: the name hook runs after the D12 gate, so only tenant-level admins hear a set name (pinned). Clause-② re-derived: no (no new export or error code; grantSetNotInCatalogMessage gains an optional param, module not re-exported from the package index).",
      "tests": "plugin-security grant-permission-set-name.test.ts +7 cells (23 passed); plugin-auth admin-user-endpoints.test.ts refusal message + widget pin (99 passed); platform-objects sys-user-create-user-email-required.test.ts +2 cells source pin (6 passed); ui ActionDesigner.paramVocabulary.test.tsx +4 cells (23 passed); qa/dogfood create-user-permission-set.dogfood.test.ts G2 message + new G4 label case on the real showcase route (4 passed). Ablations: A1 old sentence -> 6 red; A2 no label lookup -> 4 red; A3 agent exclusion removed -> 1 red; A4 reach cell with tenant-admin caller -> red; B1 old ':' joiner -> 1 red; B2 widget removed -> plugin-auth stays green (reads platform-objects dist), hence source pin C1 -> red; D1 preview ignores widget -> red.",
      "gates": "dispatch-gates --commands at b5c69de7: 71 families; --ran: 71 derived, 70 run exit 0, 1 NOT MEASURED (node scripts/check-plugin-teardown-shape.mjs --self-test exit 3: positive control pinned to commit 621a4876, unreachable in shallow clone; checker-health only), 0 UNRUN. pnpm lint exit 0; typecheck + test exit 0 for platform-objects, plugin-security, plugin-auth. Full monorepo pnpm test not run. CI pending at report time.",
      "line_budget": "13 files, +354 / -27 (381 changed lines), under the 3000 human-merge threshold",
      "files_changed": [
        ".changeset/30117-action-preview-param-widget.md",
        ".changeset/30117-create-user-permission-set-picker.md",
        ".changeset/30117-create-user-refusal-sentence.md",
        ".changeset/30117-grant-refusal-business-wording.md",
        "packages/platform-objects/src/identity/sys-user-create-user-email-required.test.ts",
        "packages/platform-objects/src/identity/sys-user.object.ts",
        "packages/plugins/plugin-auth/src/admin-user-endpoints.test.ts",
        "packages/plugins/plugin-auth/src/admin-user-endpoints.ts",
        "packages/plugins/plugin-security/src/grant-permission-set-name.test.ts",
        "packages/plugins/plugin-security/src/grant-permission-set-name.ts",
        "packages/qa/dogfood/test/create-user-permission-set.dogfood.test.ts",
        "ui/packages/app-shell/src/views/metadata-admin/ActionDesigner.paramVocabulary.test.tsx",
        "ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx"
      ],
      "mcp_calls": "subscribe_pr_activity (PR #30701, read-side); no GitHub MCP write",
      "api_writes": "git push x2 (branch), relay writes: pr_create (#30701), this comment",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: docs gap (not a defect); reach: none; content/docs/permissions/authentication.mdx 'Create a User Directly' does not document the optional permissionSet; carrier: PR Skills-docs line follow-up"
      ],
      "deviations": [
        "Card thread on the platform-assigned branch claude/project-thread-w9414q (claim amended 6670441112).",
        "Test-only file outside the order's surface: packages/qa/dogfood/test/create-user-permission-set.dogfood.test.ts (seat accepted).",
        "The refusal names the matching set when the refused value is a set's label (beyond rewording); seat asked it stay inside grant reach, pinned.",
        "ActionPreview mocks permission-ref as a picker (one key past PARAM_TYPE_OPTS).",
        "First push refused by check:commit-card-trailers (Fixes line in commit); reworded before publishing, no history rewrite."
      ]
    }
  14. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Claim: PM loop round 1 · 2026-11-05T22:45Z
    Session: session_016WV4cytQjG9X1CrCgytUM5 (seat) · dev card thread session_016WV4cwFtiPisvhAkegNCLQ
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-w9414q (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30117
    Supersedes: claim 6670353789 and its amendments 6670441112, 6670573490 (re-posted whole so the closing-target guard reads the live branch)
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/platform-objects/src/identity/sys-user.object.ts (create_user's inline permissionSet param takes widget: 'permission-ref'; the stale "widget hint params do not inherit" comment retired) and its tests; the shared grant refusal wording in packages/plugins/plugin-auth/src/admin-user-endpoints.ts (prefix) and packages/plugins/plugin-security/src/grant-permission-set-name.ts (inner sentence), said once for every grant path, with their tests; ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx runtimeWidgetFor passing widget through (note 6662463937 line 2); one root .changeset/ file per package moved. packages/qa/dogfood/test/create-user-permission-set.dogfood.test.ts (one real-route case). ⛔ packages/spec/** (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: platform-objects' create_user action declares permissionSet as a free-text param only a machine name satisfies | none: Setup shows labels and nothing on the form lists the choices | every administrator creating a user from the Console (measured on the card: a label answers 400)
    Thread-read: 6671486894
    Premise: holds — unblocked: the wait was on #30259 (closed 2026-11-05T10:04Z) and #30260 (closed 2026-11-05T16:25Z via PR #30543, merged), so widget on an action param is live on origin/main; re-derived: sys-user.object.ts still declares permissionSet with the stale comment (:359/:365 at claim time)
    Serial constraints cleared: none: no open PR touches sys-user.object.ts, admin-user-endpoints.ts, grant-permission-set-name.ts or ActionPreview.tsx (19 open non-mailbox PRs read at claim time)

  15. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5

    Landed · 2026-11-06T00:39Z · PR #30701 merged through the queue as 8fed00f949. ACCEPT 6672963276.

    Reading Value
    PR state merged, closed this card via Fixes #30117
    Check-runs at d2c87c84 56 success, 8 skipped, 0 failed

    pm:dispatched removed; the card is closed. Docs follow-up candidate, noted in the ACCEPT: content/docs/permissions/authentication.mdx ("Create a User Directly") never documented the optional permissionSet (predates this PR).

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

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions