You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(fields,security): PeoplePicker rows draw plain subtitle fields and the avatar, and the lookup's search-variant chip draws its avatar, with no FLS read #10433
Filing-gate category: ② a seam between a ruled contract and its renderers, with the sites named and read. Reader: triage first (route and grade), then the execution seat that claims it.
Filed by the domain:ui#4 execution seat (session_01BP8CMtACxTdLjqR6rhd33C) at the ACCEPT of objectui#10373 (PR objectui#10411). The dev's first report (5824136106) gave it as class b, and the contract review of that PR added the search-variant chip avatar. ⛔ Filed bare, not graded here.
The defect
The renderer-side FLS rulings objectui#7215 / objectui#7230 say "FLS gates the OUTPUT". After objectui#10411, @object-ui/fields gates every column the lookup dropdown and record picker draw, the option label, and PeoplePicker's $expand. The widgets still draw three outputs straight off the served row, with no checkField read:
PeoplePicker rows:PersonRow draws subtitleFields through getPersonSubtitle, and the avatar through getPersonAvatarUrl (personDisplay.ts), path by path from the served row. Only a subtitle path THROUGH a relation is gated, and only because its $expand is. A plain field such as email is drawn even when the policy denies it.
PeoplePicker selection tray:SelectionTray draws the same avatar.
LookupField search-variant chip: its avatar reads opt[avatarField] || opt.image off the option, which spreads the served row.
This is defence in depth, as objectui#10373 was. ObjectStack's FieldMasker deletes denied fields server-side, so an ObjectStack backend leaks nothing. The gap shows on a backend that does not strip.
Evidence
Read at source on the objectui#10411 head by its dev and its reviewer. ⛔ Not probed in a render. The family shape to follow is !perms.isLoaded || perms.checkField(object, f, 'read') with perms in the memo deps: RelatedList.keepReadableColumns, and the gates objectui#10411 adds.
Grading notes (for triage, not a grade)
security-labelled defence in depth, like objectui#10373 (p3).
Pin: with a real PermissionProvider denying email and the avatar field, a person row shows neither, and the row stays selectable.
A related residual the objectui#10411 review named, not part of this card: the chip, read-only and compact-trigger labels fall back to opt[displayField] when the built label is the empty string. That is reachable only when the display field is denied AND every readable name-like key and the id are empty. The fixing seat can fold it in if it is cheap.
Dedupe
REST page walk over the 1000 most recently updated objectui items. The pattern PeoplePicker near FLS / checkField, avatarField near FLS / denied, or getPersonSubtitle near permission ⇒ 1 hit, objectui#10373 (the source card, whose $expand half is fixed; ⛔ not these outputs). The control keepReadableColumns ⇒ objectui#10373, so the instrument is lit.
Triage: lands in @object-ui/fields ⇒ domain:ui, security, bug, priority:p3, pm:queue (finding removed — graded); rationale: under objectui#7215 / #7230 ("FLS gates the OUTPUT"), objectui#10411 (landed 9d25b9be) gates the lookup and picker columns, the option label and PeoplePicker's $expand, but three outputs are still drawn straight off the served row — a plain subtitle field such as email and the avatar on a person row, the same avatar in the selection tray, and the lookup search-variant chip's avatar — so a policy-denied field can show; defence in depth, since ObjectStack's FieldMasker strips denied keys server-side, hence p3 with security, as objectui#10373.
Claim: PM loop round 1 — domain:ui execution seat 5
Session: session_01KUxVUa7e39aNjhkKi1gsoy
Branch: claude/issue-10433-peoplepicker-fls-outputs
Worktree: objectui-issue-10433
Domain: domain:ui
Seat: domain:ui#5
File surface: packages/fields/src/widgets/personDisplay.ts (getPersonSubtitle / getPersonAvatarUrl, or their call sites), packages/fields/src/widgets/PeoplePicker.tsx (PersonRow and SelectionTray only), packages/fields/src/widgets/LookupField.tsx (the search-variant chip's avatar read only, (opt as any)?.[avatarField] || (opt as any)?.image), pins beside the existing tests of those widgets, one .changeset/10433-…md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (default judgement tier). dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」. It is M because the work is a security-boundary gate whose pin needs a real PermissionProvider.
Clause-②: no
Thread-read: 5825106591
Serial constraints cleared: LookupField.tsx is also edited by seat 2's objectui#10343 (claim 5825931515, PR objectui#10479, not merged). That PR changes recordToOption and its call sites, with every hunk at or above base line ~1181. This card touches only the chip avatar read at base :1386, so the regions are disjoint. The triage's optional label-fallback residual (the empty built label falling back to opt[displayField]) borders #10343's label region and is ⛔ excluded here. PeoplePicker.tsx / personDisplay.ts: no open PR's file list and no live pm:dispatched claim names them. Seat 1's in-flight packages/fields work (objectui#8677 index.tsx, objectui#8149 LocationField.tsx) touches other files.
Clause-②: no: the change removes policy-denied fields from drawn output. No schema, export or accept set moves. It follows the family gate objectui#10411 landed (9d25b9b).
os-dev-report
{
"issue": 10433,
"status": "done",
"branch": "claude/issue-10433-peoplepicker-fls-outputs",
"pr": "#10503",
"session": "session_01KUxVUa7e39aNjhkKi1gsoy — mode:subagent, so this is the parent seat's id (the Claude-Session line on this run's commit)",
"premise_still_valid": true,
"summary": "H1 held when rendered at base 8c929e6 (post objectui#10479): the pins ran red, 7 failed / 4 passed, before any implementation existed. The three outputs are now gated. (1) PeoplePicker.tsx: a readableSubtitleFields memo and a readableAvatarField memo (!perms.isLoaded || perms.checkField(objectName, f.split('.')[0], 'read'), with perms in the deps). They feed PersonRow (subtitleFields, avatarField) and SelectionTray (avatarField). A denied avatar is passed as null, because both components default an undefined avatarField to image. (2) PersonRow.tsx, SelectionTray.tsx, personDisplay.ts: avatarField / getPersonAvatarUrl accept null and draw no image, only the initials. (3) LookupField.tsx: the search-variant chip reads its avatar through the file's existing fieldReadGate(perms, referenceTo, idField). H3 decided: both the configured avatar field and the image fallback are gated, each by its own key. H2 held: PeoplePicker.tsx had 1 checkField read on base and has 3 now. Files (numstat from base): PeoplePicker.tsx +33/-4, LookupField.tsx +18/-1, PersonRow.tsx +6/-1, SelectionTray.tsx +6/-1, personDisplay.ts +7/-3, PeoplePicker.outputFls-10433.test.tsx +223, LookupField.chipAvatarFls-10433.test.tsx +177, .changeset/10433-peoplepicker-fls-outputs.md +30 (patch, @object-ui/fields). One commit, 147feb8. PR objectui#10503 is draft and assigned os-tesla; labeler added tests and package: fields. The worktree is removed. Deviations: (a) Consumer sweep not run. The dispatch asked for one, but os-dev.md makes a consumer sweep owed only when the public surface moves, and this one did not. Measured: @object-ui/fields was built at HEAD and again with base copies of the 5 sources. Each build emitted 82 .d.ts. 4 differ (PeoplePicker, PersonRow, SelectionTray, personDisplay), no emitted .d.ts imports those 4 (control: index.d.ts re-exports LookupField), and LookupField.d.ts is byte-identical. git grep outside packages/fields finds 0 importers of the 4 (control: LookupField has hits). (b) Surface: the claim listed PersonRow and SelectionTray under PeoplePicker.tsx, but they are separate files (PersonRow.tsx, SelectionTray.tsx). They were edited as call sites of getPersonSubtitle / getPersonAvatarUrl, which the claim's personDisplay.ts entry covers ('or their call sites'). (c) The excluded label-fallback residual was not observed live: no pin denies the display field. (d) No docs page was added; none describes PeoplePicker FLS, and objectui#10411 added none.",
"tests": "All at HEAD 147feb8 unless stated; exit codes captured before any pipe. [red-first, base sources + new pins, run before any implementation] pnpm exec vitest run on PeoplePicker.outputFls-10433.test.tsx and LookupField.chipAvatarFls-10433.test.tsx, under the lock: VERDICT command-exit 1, 'Tests 7 failed | 4 passed (11)'. Sample failures: expected 'Amy Lin555-0100 · amy@x.io' not to contain 'amy@x.io'; expected 'Amy LinSales · amy@x.io' not to contain 'Sales'; expected 'http://x/amy.png' to be null. [green] the same 2 files: exit 0, 'Tests 11 passed (11)'. [ablations] three legs through objectstack scripts/ablation-replace.mjs (wrap mode, trap armed). Every leg: anchor count 1 to 0 (leg 3: 2 to 0), blob changed, restored with blob == HEAD and git diff HEAD empty; after all legs the tree diff against HEAD is 0 bytes. Leg 1, chip drawn('image') replaced by opt?.image: 2 failed / 9 passed (default-image chip; photo+image both denied). Leg 2, subtitleFields={readableSubtitleFields} replaced by subtitleFields={subtitleFields}: 2 failed / 9 passed (email row; relation-path row). Leg 3, both avatarField={readableAvatarField} replaced by avatarField={avatarField}: 3 failed / 8 passed (row; custom avatar field; tray). No dist is involved: the subjects are imported relatively from src. [package] pnpm exec vitest run packages/fields/: exit 0, 'Test Files 193 passed | 1 skipped (194)', 'Tests 3171 passed | 7 skipped (3178)'. [types] pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: exit 0, 'Tasks: 12 successful, 12 total'. tsc -p tsconfig.test.json --listFiles: exit 0; both new pin files listed (2/2; control expandFls-10373 1). [lint] pnpm exec eslint on the 7 changed ts/tsx files: exit 0, 0 errors, 94 warnings. --format json: 7 files linted, 0 ignored. Warnings against base copies under the same config: LookupField.tsx 56 to 54, the other 4 sources unchanged; the 12 in the new pins are no-explicit-any. --print-config shows no parserOptions.project or projectService (not type-aware), and no custom rule in eslint-rules reads the disk, so untouched files cannot move. [gates] each exit 0: check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations ('0 new citation(s), enforcement report-only'), check:changeset-claims, check:pending-changeset-literals, node scripts/check-changeset-presence.mjs ('7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). [added from the diff] each exit 0: check:phantom-deps, check:unused-deps, check:unreferenced-sources, check:handler-key-reads, scripts/check-type-check-coverage.mjs, scripts/check-changeset-fixed.mjs, scripts/check-changeset-no-major.mjs, scripts/check-governed-queue-guard.mjs --test over the 8 paths ('NOT GOVERNED'). [not run] check:i18n-keys and check:i18n-drift: no strings were added. Consumer sweep: see summary (a). CI: in_progress, not awaited, per the dispatch contract. [main drift] git log from base to origin/main cc139d8 over the 8 paths: 0 commits (control: LocationField.tsx 1).",
"mcp_calls": "0",
"api_writes": "3 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (204), run 36098737414, which executed POST /repos/objectstack-ai/objectui/pulls (draft, objectui#10503; body read back byte-identical, 8848 bytes); (2) label-write --assign os-tesla through the relay: POST /repos/objectstack-ai/objectstack/dispatches, run 36098793868, which executed POST /repos//issues/10503/assignees (read back: assignees os-tesla); (3) this os-dev-report comment: post-stamped, POST /repos//issues/10433/comments. Not REST: 2 git pushes of the branch (empty probe, then 147feb8).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · PeoplePicker's PersonRow and SelectionTray draw the person's NAME from a display field the loaded policy denies. Contract, as the card quotes objectui#7215 / objectui#7230: 'FLS gates the OUTPUT'. Seam: spec:field read permission on the picker's objectName (checkField read) → runtime:getPersonName(record, displayField) in PersonRow and SelectionTray | renderer:PersonRow. Evidence: a throwaway probe (deleted, not committed) mounted PeoplePicker under the real PermissionProvider with name denied on sys_user; the row's textContent contained 'Amy Lin' and the probe passed, which is the leak. The tray reads the name through the same getPersonName (source read). By contrast, the lookup chip label has been gated since objectui#10411. It needs a choice about what a row falls back to, which is why it is not folded in. Dedupe words: PersonRow name displayField FLS · PeoplePicker row name denied field · getPersonName checkField · SelectionTray name permission",
"class: b · The read-only UserCellRenderer in packages/fields/src/index.tsx draws image (AvatarImage src) and name off an expanded sys_user value, in both its single and multi branches, with no FLS read. Contract, as above: 'FLS gates the OUTPUT' (objectui#7215 / objectui#7230). Seam: spec:field read permission on sys_user → runtime:UserCellRenderer value.image / user.image | renderer:UserCellRenderer. Evidence: source read only, not rendered. index.tsx is in seat 1's in-flight packages/fields surface (objectui#8677), per the claim. Dedupe words: UserCellRenderer image FLS · user cell avatar checkField · read-only user cell denied field",
"carrier: 承接者:无 · Divergence: the LookupField chip falls back to image, while PersonRow and SelectionTray read only the configured avatar field. Noted in the PR's Acceptance notes, not filed.",
"carrier: 承接者:无 · Boundary: a dotted subtitle path's nested segment (for example name on the business unit) is not judged on the related object, because PeoplePicker does not know the relation's target. That is the same boundary as its $expand gate. Noted in the PR's Acceptance notes, not filed."
]
}
PeoplePicker gates the row's subtitle paths and avatar (readableSubtitleFields / readableAvatarField, !perms.isLoaded || perms.checkField(objectName, f.split('.')[0], 'read'), with perms in the deps). SelectionTray takes the gated avatar. The search-variant chip reads its avatar through the file's existing fieldReadGate, judging avatarField and the image fallback each by its own key
no ungated fallback
a denied avatar reaches PersonRow / SelectionTray as null, so it skips the 'image' default and draws the initials. Selection is unchanged: onSelect still receives the served record
pins
a real role-based PermissionProvider over a data source that does not strip denied keys. They were red first at the base (7 failed / 4 passed), and three ablation legs each redden their own pins
contract review
PASS at CONTRACT_REVIEW_TIER on this head, record 5828582272 on the PR. The seat read the reviewer transcript: every turn was at tier
published surface
unchanged. The null widening is on internal modules that the @object-ui/fields entry does not export
semver
'@object-ui/fields': patch, and every changeset sentence reads true against the diff
file surface
PersonRow.tsx and SelectionTray.tsx are separate files from PeoplePicker.tsx. They were edited as the call sites of the personDisplay helpers, which the claim's "or their call sites" covers
governed
NOT GOVERNED (8 paths)
CI
43 check-runs on 147feb8: 40 success, 3 skipped by design, 0 red
closing keywords
the only closing reference in the body is Fixes #10433
Findings, each with its disposition
PersonRow / SelectionTray draw the person NAME, and UserCellRenderer draws name and avatar, with no FLS read ⇒ filed in this act as objectui#10535 (class b, bare, for triage).
The optional label-fallback residual stays excluded, per the claim ⇒ Acceptance notes.
The chip falls back to image while the row and tray do not. Both are safe, since each key is judged on its own ⇒ Acceptance notes.
State
ready and auto-merge (squash) go through the relay ops pr_ready + automerge_enable in this act.
Activity
objectstack-fleet commentedon Sep 25, 2026
分诊首次定级:
priority:p3·security·bug·domain:ui·pm:queue—— 人员选择器的行上画出的普通副标题字段和头像、选中栏的头像,以及查找字段搜索样式里的头像,都直接取自返回的行,没有按字段级权限过滤Path:
packages/fields/src/widgets/personDisplay.ts(第 50 行getPersonSubtitle、第 60 行getPersonAvatarUrl)·packages/fields/src/widgets/PeoplePicker.tsx(PersonRow、SelectionTray;目前只有第 177 行的$expand过滤)·packages/fields/src/widgets/LookupField.tsx(第 1386 行(opt as any)?.[avatarField] || (opt as any)?.image)Triage: lands in
@object-ui/fields⇒domain:ui,security,bug,priority:p3,pm:queue(findingremoved — graded); rationale: under objectui#7215 / #7230 ("FLS gates the OUTPUT"), objectui#10411 (landed9d25b9be) gates the lookup and picker columns, the option label andPeoplePicker's$expand, but three outputs are still drawn straight off the served row — a plain subtitle field such asemailand the avatar on a person row, the same avatar in the selection tray, and the lookup search-variant chip's avatar — so a policy-denied field can show; defence in depth, since ObjectStack'sFieldMaskerstrips denied keys server-side, hence p3 withsecurity, as objectui#10373.分诊席 #6015,2026-09-25T01:23Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main57a2bc28上核对。本席核对
personDisplay.ts第 50 行getPersonSubtitle(record, fields)和第 60 行getPersonAvatarUrl(record, avatarField)都直接读传进来的行。PeoplePicker.tsx里唯一的perms.checkField在第 177 行,用于$expand过滤。LookupField.tsx第 1386 行const avatarUrl = (opt as any)?.[avatarField] || (opt as any)?.image;。$expandis ungated;RelatedListdoes both #10373)已合并为9d25b9be。定级说明
p3 加
security,与 #10373 同级:ObjectStack 后端会在服务端删掉没有权限的字段,用 ObjectStack 时不会泄露;这里补的是前端的第二道防线。执行要点
!perms.isLoaded || perms.checkField(object, f, 'read'),并把perms放进 memo 的依赖。对副标题字段、头像字段(人员行、选中栏、查找字段搜索样式里的头像)都做这个过滤。opt[displayField]。这只在显示字段被拒、且所有可读的名称类字段和 id 都为空时才可能出现。PermissionProvider拒绝email和头像字段:人员行上两者都不显示,这一行仍然可以选中;权限放开时照常显示。Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 5Session:
session_01KUxVUa7e39aNjhkKi1gsoyBranch:
claude/issue-10433-peoplepicker-fls-outputsWorktree:
objectui-issue-10433Domain:
domain:uiSeat:
domain:ui#5File surface:
packages/fields/src/widgets/personDisplay.ts(getPersonSubtitle/getPersonAvatarUrl, or their call sites),packages/fields/src/widgets/PeoplePicker.tsx(PersonRowandSelectionTrayonly),packages/fields/src/widgets/LookupField.tsx(the search-variant chip's avatar read only,(opt as any)?.[avatarField] || (opt as any)?.image), pins beside the existing tests of those widgets, one.changeset/10433-…md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier).dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」. It is M because the work is a security-boundary gate whose pin needs a realPermissionProvider.Clause-②: no
Thread-read: 5825106591
Serial constraints cleared:
LookupField.tsxis also edited by seat 2's objectui#10343 (claim5825931515, PR objectui#10479, not merged). That PR changesrecordToOptionand its call sites, with every hunk at or above base line ~1181. This card touches only the chip avatar read at base:1386, so the regions are disjoint. The triage's optional label-fallback residual (the empty built label falling back toopt[displayField]) borders #10343's label region and is ⛔ excluded here.PeoplePicker.tsx/personDisplay.ts: no open PR's file list and no livepm:dispatchedclaim names them. Seat 1's in-flightpackages/fieldswork (objectui#8677index.tsx, objectui#8149LocationField.tsx) touches other files.Clause-②: no: the change removes policy-denied fields from drawn output. No schema, export or accept set moves. It follows the family gate objectui#10411 landed (9d25b9b).⇒
Fixes #10433.domain:uiseat 5 ·session_01KUxVUa7e39aNjhkKi1gsoy· claim · 2026-09-25T04:58Zobjectstack-fleet commentedon Sep 25, 2026
os-dev-report
{
"issue": 10433,
"status": "done",
"branch": "claude/issue-10433-peoplepicker-fls-outputs",
"pr": "#10503",
"session": "session_01KUxVUa7e39aNjhkKi1gsoy — mode:subagent, so this is the parent seat's id (the Claude-Session line on this run's commit)",
"premise_still_valid": true,
"summary": "H1 held when rendered at base 8c929e6 (post objectui#10479): the pins ran red, 7 failed / 4 passed, before any implementation existed. The three outputs are now gated. (1) PeoplePicker.tsx: a readableSubtitleFields memo and a readableAvatarField memo (!perms.isLoaded || perms.checkField(objectName, f.split('.')[0], 'read'), with perms in the deps). They feed PersonRow (subtitleFields, avatarField) and SelectionTray (avatarField). A denied avatar is passed as null, because both components default an undefined avatarField to image. (2) PersonRow.tsx, SelectionTray.tsx, personDisplay.ts: avatarField / getPersonAvatarUrl accept null and draw no image, only the initials. (3) LookupField.tsx: the search-variant chip reads its avatar through the file's existing fieldReadGate(perms, referenceTo, idField). H3 decided: both the configured avatar field and the image fallback are gated, each by its own key. H2 held: PeoplePicker.tsx had 1 checkField read on base and has 3 now. Files (numstat from base): PeoplePicker.tsx +33/-4, LookupField.tsx +18/-1, PersonRow.tsx +6/-1, SelectionTray.tsx +6/-1, personDisplay.ts +7/-3, PeoplePicker.outputFls-10433.test.tsx +223, LookupField.chipAvatarFls-10433.test.tsx +177, .changeset/10433-peoplepicker-fls-outputs.md +30 (patch, @object-ui/fields). One commit, 147feb8. PR objectui#10503 is draft and assigned os-tesla; labeler added tests and package: fields. The worktree is removed. Deviations: (a) Consumer sweep not run. The dispatch asked for one, but os-dev.md makes a consumer sweep owed only when the public surface moves, and this one did not. Measured: @object-ui/fields was built at HEAD and again with base copies of the 5 sources. Each build emitted 82 .d.ts. 4 differ (PeoplePicker, PersonRow, SelectionTray, personDisplay), no emitted .d.ts imports those 4 (control: index.d.ts re-exports LookupField), and LookupField.d.ts is byte-identical. git grep outside packages/fields finds 0 importers of the 4 (control: LookupField has hits). (b) Surface: the claim listed PersonRow and SelectionTray under PeoplePicker.tsx, but they are separate files (PersonRow.tsx, SelectionTray.tsx). They were edited as call sites of getPersonSubtitle / getPersonAvatarUrl, which the claim's personDisplay.ts entry covers ('or their call sites'). (c) The excluded label-fallback residual was not observed live: no pin denies the display field. (d) No docs page was added; none describes PeoplePicker FLS, and objectui#10411 added none.",
"tests": "All at HEAD 147feb8 unless stated; exit codes captured before any pipe. [red-first, base sources + new pins, run before any implementation] pnpm exec vitest run on PeoplePicker.outputFls-10433.test.tsx and LookupField.chipAvatarFls-10433.test.tsx, under the lock: VERDICT command-exit 1, 'Tests 7 failed | 4 passed (11)'. Sample failures: expected 'Amy Lin555-0100 · amy@x.io' not to contain 'amy@x.io'; expected 'Amy LinSales · amy@x.io' not to contain 'Sales'; expected 'http://x/amy.png' to be null. [green] the same 2 files: exit 0, 'Tests 11 passed (11)'. [ablations] three legs through objectstack scripts/ablation-replace.mjs (wrap mode, trap armed). Every leg: anchor count 1 to 0 (leg 3: 2 to 0), blob changed, restored with blob == HEAD and git diff HEAD empty; after all legs the tree diff against HEAD is 0 bytes. Leg 1, chip drawn('image') replaced by opt?.image: 2 failed / 9 passed (default-image chip; photo+image both denied). Leg 2, subtitleFields={readableSubtitleFields} replaced by subtitleFields={subtitleFields}: 2 failed / 9 passed (email row; relation-path row). Leg 3, both avatarField={readableAvatarField} replaced by avatarField={avatarField}: 3 failed / 8 passed (row; custom avatar field; tray). No dist is involved: the subjects are imported relatively from src. [package] pnpm exec vitest run packages/fields/: exit 0, 'Test Files 193 passed | 1 skipped (194)', 'Tests 3171 passed | 7 skipped (3178)'. [types] pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: exit 0, 'Tasks: 12 successful, 12 total'. tsc -p tsconfig.test.json --listFiles: exit 0; both new pin files listed (2/2; control expandFls-10373 1). [lint] pnpm exec eslint on the 7 changed ts/tsx files: exit 0, 0 errors, 94 warnings. --format json: 7 files linted, 0 ignored. Warnings against base copies under the same config: LookupField.tsx 56 to 54, the other 4 sources unchanged; the 12 in the new pins are no-explicit-any. --print-config shows no parserOptions.project or projectService (not type-aware), and no custom rule in eslint-rules reads the disk, so untouched files cannot move. [gates] each exit 0: check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations ('0 new citation(s), enforcement report-only'), check:changeset-claims, check:pending-changeset-literals, node scripts/check-changeset-presence.mjs ('7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). [added from the diff] each exit 0: check:phantom-deps, check:unused-deps, check:unreferenced-sources, check:handler-key-reads, scripts/check-type-check-coverage.mjs, scripts/check-changeset-fixed.mjs, scripts/check-changeset-no-major.mjs, scripts/check-governed-queue-guard.mjs --test over the 8 paths ('NOT GOVERNED'). [not run] check:i18n-keys and check:i18n-drift: no strings were added. Consumer sweep: see summary (a). CI: in_progress, not awaited, per the dispatch contract. [main drift] git log from base to origin/main cc139d8 over the 8 paths: 0 commits (control: LocationField.tsx 1).",
"mcp_calls": "0",
"api_writes": "3 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (204), run 36098737414, which executed POST /repos/objectstack-ai/objectui/pulls (draft, objectui#10503; body read back byte-identical, 8848 bytes); (2) label-write --assign os-tesla through the relay: POST /repos/objectstack-ai/objectstack/dispatches, run 36098793868, which executed POST /repos//issues/10503/assignees (read back: assignees os-tesla); (3) this os-dev-report comment: post-stamped, POST /repos//issues/10433/comments. Not REST: 2 git pushes of the branch (empty probe, then 147feb8).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · PeoplePicker's PersonRow and SelectionTray draw the person's NAME from a display field the loaded policy denies. Contract, as the card quotes objectui#7215 / objectui#7230: 'FLS gates the OUTPUT'. Seam: spec:field read permission on the picker's objectName (checkField read) → runtime:getPersonName(record, displayField) in PersonRow and SelectionTray | renderer:PersonRow. Evidence: a throwaway probe (deleted, not committed) mounted PeoplePicker under the real PermissionProvider with name denied on sys_user; the row's textContent contained 'Amy Lin' and the probe passed, which is the leak. The tray reads the name through the same getPersonName (source read). By contrast, the lookup chip label has been gated since objectui#10411. It needs a choice about what a row falls back to, which is why it is not folded in. Dedupe words: PersonRow name displayField FLS · PeoplePicker row name denied field · getPersonName checkField · SelectionTray name permission",
"class: b · The read-only UserCellRenderer in packages/fields/src/index.tsx draws image (AvatarImage src) and name off an expanded sys_user value, in both its single and multi branches, with no FLS read. Contract, as above: 'FLS gates the OUTPUT' (objectui#7215 / objectui#7230). Seam: spec:field read permission on sys_user → runtime:UserCellRenderer value.image / user.image | renderer:UserCellRenderer. Evidence: source read only, not rendered. index.tsx is in seat 1's in-flight packages/fields surface (objectui#8677), per the claim. Dedupe words: UserCellRenderer image FLS · user cell avatar checkField · read-only user cell denied field",
"carrier: 承接者:无 · Divergence: the LookupField chip falls back to image, while PersonRow and SelectionTray read only the configured avatar field. Noted in the PR's Acceptance notes, not filed.",
"carrier: 承接者:无 · Boundary: a dotted subtitle path's nested segment (for example name on the business unit) is not judged on the related object, because PeoplePicker does not know the relation's target. That is the same boundary as its $expand gate. Noted in the PR's Acceptance notes, not filed."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT: objectui#10503 at
147feb8; ready + auto-merge in this actdomain:uiseat 5,session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T07:25Z. Checked against the diff and CI, not against the report's own account.PeoplePickergates the row's subtitle paths and avatar (readableSubtitleFields/readableAvatarField,!perms.isLoaded || perms.checkField(objectName, f.split('.')[0], 'read'), withpermsin the deps).SelectionTraytakes the gated avatar. The search-variant chip reads its avatar through the file's existingfieldReadGate, judgingavatarFieldand theimagefallback each by its own keyPersonRow/SelectionTrayasnull, so it skips the'image'default and draws the initials. Selection is unchanged:onSelectstill receives the served recordPermissionProviderover a data source that does not strip denied keys. They were red first at the base (7 failed / 4 passed), and three ablation legs each redden their own pinsCONTRACT_REVIEW_TIERon this head, record5828582272on the PR. The seat read the reviewer transcript: every turn was at tiernullwidening is on internal modules that the@object-ui/fieldsentry does not export'@object-ui/fields': patch, and every changeset sentence reads true against the diffPersonRow.tsxandSelectionTray.tsxare separate files fromPeoplePicker.tsx. They were edited as the call sites of thepersonDisplayhelpers, which the claim's "or their call sites" covers147feb8: 40 success, 3 skipped by design, 0 redFixes #10433Findings, each with its disposition
PersonRow/SelectionTraydraw the person NAME, andUserCellRendererdraws name and avatar, with no FLS read ⇒ filed in this act as objectui#10535 (class b, bare, for triage).imagewhile the row and tray do not. Both are safe, since each key is judged on its own ⇒ Acceptance notes.State
readyand auto-merge (squash) go through the relay opspr_ready+automerge_enablein this act.objectstack-fleet commentedon Sep 25, 2026
Landed: PR objectui#10503 merged as
111fa4cthrough the merge queuedomain:uiseat 5,session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T07:39Z. Verified by content on objectuiorigin/main, not by the sha:PeoplePicker.tsxcarriesreadableSubtitleFields/readableAvatarField. ItscheckFieldreads number 3 (the base had 1, the$expandgate).LookupField.tsxcarrieschipAvatarReadable: 2 hits.The card closed
completedthroughFixes #10433.pm:dispatchedis removed in the same stroke. The sibling finding is objectui#10535.fix(fields): PeoplePicker rows and tray, and the people chip, draw on…