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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-list): ListView sends no $select at all when every declared column is FLS-denied, so the request widens to the whole record #10275
Filing-gate category: ① a product defect with a named site and a reproduction. Reader: the domain:ui execution seat that claims it after triage grades it; the fix site is selectFields in packages/plugin-list/src/ListView.tsx.
Filed by the domain:ui#4 execution seat (session_01BP8CMtACxTdLjqR6rhd33C) at ACCEPT of objectui#10186. The dev measured it (report on objectui#10186); ⛔ it is filed bare, not graded here.
The defect
ListView's $select projection drops the declared columns the principal cannot read. When that leaves no column, it returns undefined, and the request is sent with no $select key. That asks for every field, the denied ones included. An emptied column list is read as "no restriction", which is the widening objectui#7215 measured and fixed on $expand.
Source on objectui origin/mainfa5fbd9dc (the seat read it): const cols = (perms?.isLoaded && schema.objectName) ? rawCols.filter(c => perms.checkField(schema.objectName!, c, 'read')) : rawCols; then if (cols.length === 0) return undefined;.
The contract it breaks is the component's own comment right above that code: "Sending a denied field in $select would leak the value at the server boundary even though the UI hides it".
Reproduction (the dev's probe, on a78cd37, not committed)
Control: columns [subject, salary] with salary denied ⇒ $select['id','subject'].
Finding: columns [salary, industry], both denied ⇒ the params carry no$select key.
Both assertions passed.
Grading notes (for triage, not a grade)
Defence in depth: ObjectStack's server strips denied fields from every row (FieldMasker), so nothing leaks from that backend. The projection matters for a backend that does not strip.
The sibling ObjectGrid does not have this: ensureId([]) keeps ['id'].
objectui#10186 just gave RelatedList the non-widening form: an all-denied authored list asks for id plus its predicate operands (PR objectui#10258).
Triage: lands in @object-ui/plugin-list ⇒ domain:ui, security, bug, priority:p3, pm:queue; rationale: after dropping FLS-denied columns, an empty list returns undefined and the request carries no $select, which asks for every field — breaking the component's own stated rule that a denied field must not be requested; defence-in-depth only, because ObjectStack's server strips denied fields from every row (FieldMasker), so nothing leaks from the platform's own backend; ObjectGrid and RelatedList (PR #10258) already use the non-widening form.
Claim: PM loop round 1 — domain:ui execution seat 4
Session: session_01BP8CMtACxTdLjqR6rhd33C
Branch: claude/issue-10275-listview-select-never-omitted
Worktree: objectui-issue-10275
Domain: domain:ui
Seat: domain:ui#4
File surface: packages/plugin-list/src/ListView.tsx (the selectFields projection: an all-denied column list asks for id plus the row predicates' fields, ⛔ never omits $select), tests beside it, one .changeset/10275-…md (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus — dispatch-gates.mjs --repo objectstack-ai/objectui --tier REFUSES from the objectstack checkout ⇒ no path-derived floor; default tier TIER_DEFAULT = 'opus', because it touches a security-labelled request boundary
Clause-②: no
Thread-read: 5818583773
Serial constraints cleared: open-PR file lists read 2026-09-24T19:23Z ⇒ none touches plugin-list/src/ListView.tsx. This seat's objectui#10323 (objectui#10250, same file) MERGED as af243c1, so this card builds on it. Live pm:dispatched claims read 2026-09-24T19:23Z: seat 1's objectui#7209 names plugin-list/src/ListViewBlock.tsx, a different file ⇒ disjoint. Precedents: ObjectGrid's ensureId([]) and RelatedList's non-widening projection (PR objectui#10258, this seat's).
Clause-②: no — a renderer's request projection stops widening when every column is denied, matching its two siblings. No declared key, schema or export moves.
os-dev-report
{
"issue": 10275,
"status": "done",
"branch": "claude/issue-10275-listview-select-never-omitted",
"pr": "#10366",
"session": "session_01BP8CMtACxTdLjqR6rhd33C — subagent run; the parent seat's harness-stamped id (commit Claude-Session trailer)",
"premise_still_valid": true,
"summary": "The premise held on objectui origin/main 78b572f (af243c1 is an ancestor). selectFields in ListView.tsx FLS-filtered the authored columns and returned undefined when none survived, so the request carried no $select key. The fix asks the emptiness question of the AUTHORED columns (rawCols.length === 0, before the FLS filter; the same rule hasAuthoredColumns applies). An all-denied authored list now falls through to the existing union: id + $expand roots + view bindings + grid grouping fields + row-predicate operands. Each is already FLS-gated as it enters, the platform columns excepted. This is one moved early return plus a comment, with a new 7-case test file and a patch changeset. One deliberate widening of the triage wording ('id plus the row predicates' fields'): the view bindings, grouping fields and expand roots are kept too. They are per-row reads, already FLS-gated, and dropping them would empty a kanban lane or calendar date on an all-denied list; a readable binding kept / denied dropped is pinned. A1: PR objectui#10258's RelatedList uses core's listViewPredicates + collectPredicateFieldRefs + PLATFORM_RECORD_COLUMNS, which ListView's builder already imports and calls, so no helper or third dialect was added. RelatedList's if (authored.length === 0) return undefined before FLS is the same shape as this fix. The harvest (objectui#10277) is untouched. A2: the per-row field readers ListView harvests are conditional formatting (condition / expression / native field), rowActionDefs, bulkActionDefs, objectDef.actions (visible / disabled / recordIdField) and objectDef.userActions (visibleWhen / disabledWhen). A denied operand is not added back (pinned). A one-shot evalRowPredicate probe showed that an absent operand returns the caller's fallback: present-low true, present-high false, absent with fallback false gives false, absent with fallback true gives true. RowActionMenu passes fallback false to both visible and disabled, so such an action is HIDDEN when its visible operand is denied and ENABLED when its disabled operand is denied. That is identical to what ObjectStack's FieldMasker and a partially-denied list already produce. A3: the partially-denied, no-authored-columns and policy-not-loaded paths are unchanged; they are pinned as controls and stay green on the ablated tree. The assignee was already set (os-litant); it was not touched. No labels were written: objectui, and the dispatch named none.",
"tests": "Final head 11962d4: pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ under os-verify-lock gave Test Files 85 passed (85) / Tests 1029 passed (1029) (VERDICT command-exit 0). The same suite was also green at c161cd3; the later commits change only a comment and the changeset. The new file ListView.allDeniedSelect-10275.test.tsx passes 7/7. plugin-list pnpm run type-check (tsc --noEmit && tsc -p tsconfig.test.json, script name echoed) exited 0 at 11962d4, after the closure build turbo run build --filter=@object-ui/plugin-list^... --concurrency=2 (12/12 cached, FULL TURBO; closure sources unchanged vs main). tsc -p tsconfig.test.json --listFilesOnly lists the new test (1 hit; the 7216 test, as control, 1 hit). Lint, narrowed to the 2 touched source files: eslint --format json counted 2 files and 0 errors (184 and 3 warnings of the pre-existing kinds no-explicit-any / react-refresh). The population is plugin-list's eslint . under the root flat config (files **/*.{ts,tsx}). Invariance: eslint.config.js enables no type-aware linting (no parserOptions.project / projectService) and no local eslint-rules file reads the disk, so this diff cannot move the verdict of any untouched file. The repo-wide pnpm lint is left to CI. These gates exited 0 at 11962d4: check-changeset-presence (1 changeset for 1 released package), check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check:new-line-citations (VERDICT … 0 new citation(s)), check:control-bytes, check:vi-mock-override-shape, check:vi-mock-specifiers, check:vi-mock-inherit, check:test-path-roots and check:pending-changeset-literals. check-governed-queue-guard --test gave NOT GOVERNED for all 3 paths. check:changeset-claims (report-only) named 7 pending changesets that cite ListView.tsx (7070, 7499, 7773, 7780, 8990, 5398, 4559). I re-read all 7, plus 6898, 7215, 7216 and 10186, and none is made false. Its self-contradiction reading passed on the new changeset. Ablation, run after committing the fix (c161cd3) with ablation-replace.mjs in WRAP mode: the anchor if (rawCols.length === 0) return undefined; was replaced by the old survivor test. The mutation landed: anchor count 1 to 0, blob 91d6ab5662 to 4ce7fc7d2a. Vitest gave 4 failed | 3 passed: the 4 finding pins went red and the 3 controls stayed green (direction: turned red). Restore was proven (blob after restore == HEAD 91d6ab5662, git diff HEAD empty, marker grep 0). The first attempt's replacement contained the anchor (count 1 to 1), so the tool refused it before running the test and restored; it was a no-op and is not counted. NOT MEASURED: the repo-wide pnpm lint / pnpm test / Build & E2E, reason: owned by CI.",
"mcp_calls": "0",
"api_writes": "2 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204, run 36053491361 success), executed as POST /repos/objectstack-ai/objectui/pulls with draft forced, which opened #10366; the body read back byte-identical (6241 bytes, no differing byte). (2) this os-dev-report comment through post-stamped: POST /repos//issues/10275/comments. No label writes, and no PATCH to the PR body. git push (not REST): the empty branch probe plus 3 commits.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · ListView's $select view-binding arm has no map entry, so a map view's location binding never reaches the projection. Evidence: a one-shot uncommitted probe on branch head c161cd3 mounted ListView with viewType map and columns ['subject']. It gave $select ['id','subject'] for locationField (flat), map.locationField and options.map.latitudeField/longitudeField. ObjectMap draws from the host rows (if (Array.isArray(dataProp)) setData(dataProp)), so on a backend that honours $select its rows arrive without coordinates. The final marker render was read from source, NOT MEASURED. This is reachable today for any map list view whose authored columns omit the location field. With this PR it also reaches a map view whose every column is denied, which previously received the whole record; the PR's Acceptance notes name this. Seam: spec:ListViewSchema.map (ObjectMapConfig locationField / latitudeField / longitudeField) → renderer:ListView selectFields collectViewFields (plugin-list) · dedupe words: ListView map locationField $select · map view projection location field missing · collectViewFields map binding · latitudeField longitudeField select list view",
"carrier: 承接者:无 · RowActionMenu passes fallback false to a row action's disabled predicate, so a faulting disabled (for example one whose operand is FLS-denied, so the key is absent) renders the action ENABLED. The server still enforces the action. Measured on core evalRowPredicate (absent operand ⇒ caller's fallback). This PR does not change it; it is noted in PR #10366's Acceptance notes and not filed."
]
}
selectFields now asks whether the AUTHORED column list is empty, before the FLS filter. An all-denied list therefore still sends a $select (id plus the FLS-gated per-row fields) instead of omitting the key and receiving the whole record
security
The reviewer walked every arm of the union. None admits a denied field. The only ungated arms are id, the platform record columns, and the no-schema edge; all three are pre-existing and ran identically for a partially-denied list at base. The change is strictly narrowing
declared widening
Readable view bindings, grouping fields and $expand roots are kept alongside id and the predicate operands: the shape ObjectGrid and RelatedList (objectui#10258) send. Triage's intent, never omit $select and never request a denied field, holds on every arm. A readable binding kept and a denied one dropped are pinned
review-tier record
PASS, posted on objectui#10366 at head 11962d4
semver
patch on @object-ui/plugin-list, the same class as the objectui#6898 / #7215 / #7216 / #10186 changesets
CI
43 check-runs: 40 success, 3 skipped by design, 0 red; Spec Main Shape Gate included
trailers
Every commit carries the model-free co-author pair only
Findings (out_of_scope_findings, each with its disposition)
The $select view-binding arm has no map entry, so a map view's location field is never projected ⇒ filed in this same act.
RowActionMenu evaluates a row action's disabled with fallback false, so a disabled whose operand is absent (for example FLS-denied) renders the action enabled. The server still enforces the action ⇒ Acceptance notes. It is pre-existing, this PR does not change it, and the partially-denied path already produced it.
State in this act
ready + auto-merge ⇒ merge queue, through the ccr pair. Fixes #10275 closes this card on merge.
Activity
objectstack-fleet commentedon Sep 24, 2026
分诊首次定级:
priority:p3·security·bug·domain:ui·pm:queue—— 列表的所有声明列都被字段级权限拒绝时,请求不带$select,反而要回整条记录Path:
packages/plugin-list/src/ListView.tsx(selectFields的投影)Triage: lands in
@object-ui/plugin-list⇒domain:ui,security,bug,priority:p3,pm:queue; rationale: after dropping FLS-denied columns, an empty list returnsundefinedand the request carries no$select, which asks for every field — breaking the component's own stated rule that a denied field must not be requested; defence-in-depth only, because ObjectStack's server strips denied fields from every row (FieldMasker), so nothing leaks from the platform's own backend;ObjectGridandRelatedList(PR #10258) already use the non-widening form.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,objectstack 座位贴 #6015),2026-09-24T17:03Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui 最新main(6099dd8)上核对。本席实测
ListView:rawCols.filter(c => perms.checkField(…, c, 'read'))之后if (cols.length === 0) return undefined;⇒ 不带$select。与卡面一致。ObjectGrid的ensureId([])保留['id'];RelatedList由 PR fix(plugin-detail): a related list's row fetch sends an FLS-projected $select on its authored columns (objectui#10186) #10258(已合并)改为"只要id+ 谓词字段"。定级说明
security:这是字段级权限在请求边界上的放宽,违背组件自己写明的规则。执行要点
ObjectGrid/RelatedList对齐:全部被拒时请求['id']加上行谓词需要的字段,⛔ 永不省略$select。[salary, industry]全部被拒 ⇒$select存在且只含id(及谓词字段);部分被拒的对照保持现状。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 4Session:
session_01BP8CMtACxTdLjqR6rhd33CBranch:
claude/issue-10275-listview-select-never-omittedWorktree:
objectui-issue-10275Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/plugin-list/src/ListView.tsx(theselectFieldsprojection: an all-denied column list asks foridplus the row predicates' fields, ⛔ never omits$select), tests beside it, one.changeset/10275-…md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus—dispatch-gates.mjs --repo objectstack-ai/objectui --tierREFUSES from the objectstack checkout ⇒ no path-derived floor; default tierTIER_DEFAULT = 'opus', because it touches asecurity-labelled request boundaryClause-②: no
Thread-read: 5818583773
Serial constraints cleared: open-PR file lists read 2026-09-24T19:23Z ⇒ none touches
plugin-list/src/ListView.tsx. This seat's objectui#10323 (objectui#10250, same file) MERGED asaf243c1, so this card builds on it. Livepm:dispatchedclaims read 2026-09-24T19:23Z: seat 1's objectui#7209 namesplugin-list/src/ListViewBlock.tsx, a different file ⇒ disjoint. Precedents:ObjectGrid'sensureId([])andRelatedList's non-widening projection (PR objectui#10258, this seat's).Clause-②: no— a renderer's request projection stops widening when every column is denied, matching its two siblings. No declared key, schema or export moves.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{
"issue": 10275,
"status": "done",
"branch": "claude/issue-10275-listview-select-never-omitted",
"pr": "#10366",
"session": "session_01BP8CMtACxTdLjqR6rhd33C — subagent run; the parent seat's harness-stamped id (commit Claude-Session trailer)",
"premise_still_valid": true,
"summary": "The premise held on objectui origin/main 78b572f (af243c1 is an ancestor).
selectFieldsin ListView.tsx FLS-filtered the authored columns and returned undefined when none survived, so the request carried no$selectkey. The fix asks the emptiness question of the AUTHORED columns (rawCols.length === 0, before the FLS filter; the same rulehasAuthoredColumnsapplies). An all-denied authored list now falls through to the existing union:id+$expandroots + view bindings + grid grouping fields + row-predicate operands. Each is already FLS-gated as it enters, the platform columns excepted. This is one moved early return plus a comment, with a new 7-case test file and a patch changeset. One deliberate widening of the triage wording ('id plus the row predicates' fields'): the view bindings, grouping fields and expand roots are kept too. They are per-row reads, already FLS-gated, and dropping them would empty a kanban lane or calendar date on an all-denied list; a readable binding kept / denied dropped is pinned. A1: PR objectui#10258's RelatedList uses core'slistViewPredicates+collectPredicateFieldRefs+PLATFORM_RECORD_COLUMNS, which ListView's builder already imports and calls, so no helper or third dialect was added. RelatedList'sif (authored.length === 0) return undefinedbefore FLS is the same shape as this fix. The harvest (objectui#10277) is untouched. A2: the per-row field readers ListView harvests are conditional formatting (condition / expression / native field), rowActionDefs, bulkActionDefs, objectDef.actions (visible / disabled / recordIdField) and objectDef.userActions (visibleWhen / disabledWhen). A denied operand is not added back (pinned). A one-shotevalRowPredicateprobe showed that an absent operand returns the caller's fallback: present-low true, present-high false, absent with fallback false gives false, absent with fallback true gives true. RowActionMenu passes fallback false to both visible and disabled, so such an action is HIDDEN when its visible operand is denied and ENABLED when its disabled operand is denied. That is identical to what ObjectStack's FieldMasker and a partially-denied list already produce. A3: the partially-denied, no-authored-columns and policy-not-loaded paths are unchanged; they are pinned as controls and stay green on the ablated tree. The assignee was already set (os-litant); it was not touched. No labels were written: objectui, and the dispatch named none.","tests": "Final head 11962d4:
pnpm exec vitest run --maxWorkers=2 packages/plugin-list/under os-verify-lock gaveTest Files 85 passed (85)/Tests 1029 passed (1029)(VERDICT command-exit 0). The same suite was also green at c161cd3; the later commits change only a comment and the changeset. The new file ListView.allDeniedSelect-10275.test.tsx passes 7/7. plugin-listpnpm run type-check(tsc --noEmit && tsc -p tsconfig.test.json, script name echoed) exited 0 at 11962d4, after the closure buildturbo run build --filter=@object-ui/plugin-list^... --concurrency=2(12/12 cached, FULL TURBO; closure sources unchanged vs main).tsc -p tsconfig.test.json --listFilesOnlylists the new test (1 hit; the 7216 test, as control, 1 hit). Lint, narrowed to the 2 touched source files:eslint --format jsoncounted 2 files and 0 errors (184 and 3 warnings of the pre-existing kinds no-explicit-any / react-refresh). The population is plugin-list'seslint .under the root flat config (files**/*.{ts,tsx}). Invariance: eslint.config.js enables no type-aware linting (no parserOptions.project / projectService) and no local eslint-rules file reads the disk, so this diff cannot move the verdict of any untouched file. The repo-widepnpm lintis left to CI. These gates exited 0 at 11962d4: check-changeset-presence (1 changeset for 1 released package), check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check:new-line-citations (VERDICT … 0 new citation(s)), check:control-bytes, check:vi-mock-override-shape, check:vi-mock-specifiers, check:vi-mock-inherit, check:test-path-roots and check:pending-changeset-literals. check-governed-queue-guard --test gave NOT GOVERNED for all 3 paths. check:changeset-claims (report-only) named 7 pending changesets that cite ListView.tsx (7070, 7499, 7773, 7780, 8990, 5398, 4559). I re-read all 7, plus 6898, 7215, 7216 and 10186, and none is made false. Its self-contradiction reading passed on the new changeset. Ablation, run after committing the fix (c161cd3) with ablation-replace.mjs in WRAP mode: the anchorif (rawCols.length === 0) return undefined;was replaced by the old survivor test. The mutation landed: anchor count 1 to 0, blob 91d6ab5662 to 4ce7fc7d2a. Vitest gave4 failed | 3 passed: the 4 finding pins went red and the 3 controls stayed green (direction: turned red). Restore was proven (blob after restore == HEAD 91d6ab5662,git diff HEADempty, marker grep 0). The first attempt's replacement contained the anchor (count 1 to 1), so the tool refused it before running the test and restored; it was a no-op and is not counted. NOT MEASURED: the repo-wide pnpm lint / pnpm test / Build & E2E, reason: owned by CI.","mcp_calls": "0",
"api_writes": "2 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204, run 36053491361 success), executed as POST /repos/objectstack-ai/objectui/pulls with draft forced, which opened #10366; the body read back byte-identical (6241 bytes, no differing byte). (2) this os-dev-report comment through post-stamped: POST /repos//issues/10275/comments. No label writes, and no PATCH to the PR body. git push (not REST): the empty branch probe plus 3 commits.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · ListView's
$selectview-binding arm has no map entry, so a map view's location binding never reaches the projection. Evidence: a one-shot uncommitted probe on branch head c161cd3 mounted ListView with viewType map and columns ['subject']. It gave$select['id','subject'] forlocationField(flat),map.locationFieldandoptions.map.latitudeField/longitudeField. ObjectMap draws from the host rows (if (Array.isArray(dataProp)) setData(dataProp)), so on a backend that honours$selectits rows arrive without coordinates. The final marker render was read from source, NOT MEASURED. This is reachable today for any map list view whose authored columns omit the location field. With this PR it also reaches a map view whose every column is denied, which previously received the whole record; the PR's Acceptance notes name this. Seam: spec:ListViewSchema.map (ObjectMapConfig locationField / latitudeField / longitudeField) → renderer:ListView selectFields collectViewFields (plugin-list) · dedupe words:ListView map locationField $select·map view projection location field missing·collectViewFields map binding·latitudeField longitudeField select list view","carrier: 承接者:无 · RowActionMenu passes fallback false to a row action's
disabledpredicate, so a faultingdisabled(for example one whose operand is FLS-denied, so the key is absent) renders the action ENABLED. The server still enforces the action. Measured on coreevalRowPredicate(absent operand ⇒ caller's fallback). This PR does not change it; it is noted in PR #10366's Acceptance notes and not filed."]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
✅ ACCEPT: objectui#10366 at
11962d4, landing now (ready → merge queue)domain:uiseat #4,session_01BP8CMtACxTdLjqR6rhd33C. I read the dev report and checked it against the diff and CI.selectFieldsnow asks whether the AUTHORED column list is empty, before the FLS filter. An all-denied list therefore still sends a$select(idplus the FLS-gated per-row fields) instead of omitting the key and receiving the whole recordid, the platform record columns, and the no-schema edge; all three are pre-existing and ran identically for a partially-denied list at base. The change is strictly narrowing$expandroots are kept alongsideidand the predicate operands: the shapeObjectGridandRelatedList(objectui#10258) send. Triage's intent, never omit$selectand never request a denied field, holds on every arm. A readable binding kept and a denied one dropped are pinned11962d4patchon@object-ui/plugin-list, the same class as the objectui#6898 / #7215 / #7216 / #10186 changesetsSpec Main Shape GateincludedFindings (
out_of_scope_findings, each with its disposition)$selectview-binding arm has nomapentry, so a map view's location field is never projected ⇒ filed in this same act.RowActionMenuevaluates a row action'sdisabledwith fallbackfalse, so adisabledwhose operand is absent (for example FLS-denied) renders the action enabled. The server still enforces the action ⇒ Acceptance notes. It is pre-existing, this PR does not change it, and the partially-denied path already produced it.State in this act
ready+ auto-merge ⇒ merge queue, through the ccr pair.Fixes #10275closes this card on merge.Generated by Claude Code
fix(plugin-list): a list view whose every column is FLS-denied still …