Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-detail): RelatedList's row fetch sends no $select, so a denied column is returned over the wire and dropped only at the column layer — the surface has objectui#6898's expand half and has never had the select half #10186

Closed

Description

@os-elon-musk

Ruled: 5814461689 · letter select (FLS-projected $select on RelatedList) · 2026-09-24T12:51Z

The finding

RelatedList's row fetch sends no $select at all, so a field the current principal cannot read is returned over the wire and dropped only afterwards, when effectiveColumns applies filterFLS to the columns. FLS is applied nowhere on the request.

Evidence: the auto-fetch params object is built with $filter, $top, $skip, $orderby — and, after objectui#10185, $expand — and no projection.

Named failing probe: render RelatedList under a permissions provider that denies one column, and assert dataSource.find is called with a $select excluding it. Red today.

⭐ Honest grading, because it changes what this is

DetailView's own comment records that ObjectStack's server masks denied fields (FieldMasker.maskRecord). ⇒ against that backend this is a defence-in-depth gap, ⛔ not a live disclosure. It is load-bearing for a backend that does not strip — which is exactly the ground the sibling gates were graded on.

⛔ This card does not claim data is leaking today. It claims the client asks for more than it may read and relies on the server to be careful.

Why it is the same class the repo has already ruled twice

objectui#6898 ruled the $select projection for ListView; objectui#7215 / #7230 extended it to $expand roots. ⇒ this surface now has the expand half and has never had the select half. The asymmetry is the finding.

⚠️ It needs a ruling before it needs a fix

⛔ Deliberately not repaired alongside objectui#10112: a related list derives its columns from rows it has not fetched yet, so narrowing the projection is not the same operation it is on ListView. Whether a related list may narrow at all — and what it does on the first fetch, before any column is known — is a design question, ⛔ not a mechanical port of the sibling gates.

Provenance and radius

Measured by the dev on objectui#10112 while adding the $expand half, ⛔ not re-derived by this seat beyond confirming the params object carries no projection. Radius: RelatedList's auto-fetch path. ⛔ Outside: the caller-supplied-data path, and every other surface in the family — the sibling gates suggest most are covered, but that was not measured here.

Filed by the domain:ui execution seat #1 (session_01Xr7APep6jm1Zta3KUzPzZf) at ACCEPT. ⛔ Filed bare.

Dedupe words

RelatedList $select projection · related list field-level security request · denied column over the wire plugin-detail · objectui#6898 select half missing related list · FLS applied to columns not query


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: UI closure batch 14 item 8 · letter select (RelatedList sends a $select projected by field-level security) · maintainer 「同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T12:50Z

    The same class was already ruled twice: objectui#6898 ($select projection for ListView) and objectui#7215 / #7230 ($expand roots). This surface has the expand half and has never had the select half.

    Ruled: add the select half, per that precedent. RelatedList's row fetch sends a $select built from the effective columns after filterFLS, so a column the principal cannot read is never requested. The named failing probe in the body is the pin: a permissions provider denying one column, and dataSource.find called with a $select that excludes it, red before and green after. Defence in depth, recorded honestly: against ObjectStack's server (FieldMasker) nothing leaks today. ⛔ No second decision card.

    Execution: pm:queue · priority:p2 · security · domain:ui stand.


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat 4
    Session: session_01BP8CMtACxTdLjqR6rhd33C
    Branch: claude/issue-10186-related-list-fls-select
    Worktree: objectui-issue-10186
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/plugin-detail/src/RelatedList.tsx (the auto-fetch params object and the FLS column derivation it reads), its tests under packages/plugin-detail/src, one .changeset/10186-…md (stop on breach; explain in the report)
    Container & model: M, 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' for a ruling-implementation card with a design edge (first fetch before columns are known)
    Clause-②: no
    Ruling-ref: 5814461689
    Thread-read: 5814461689
    Serial constraints cleared: open-PR file lists read 2026-09-24T12:52Z (15 open PRs) ⇒ none touches packages/plugin-detail/src/RelatedList.tsx; objectui#10237 edits RecordActivityTimeline.tsx only; objectui#8941 holds DetailView.tsx ⇒ ⛔ not on this surface. Live claims read 2026-09-24T12:59Z: seat 1 objectui#10184 (useRecordEditable.ts, useRecordCrudVerdicts.ts), objectui#7190 (InlineFieldInput.tsx), objectui#10062, #10024, #10015; seat 2 objectui#10044, #10046, #6326; spec@objectui objectui#10071 ⇒ disjoint files.

    Clause-②: no — the ruling adds a $select projection to an internal request; it widens no accepted shape and adds no exported surface, and a runtime permission-behaviour change is the human-floor security class, ⛔ not clause ②. Reading on objectui origin/main a78cd37 at 2026-09-24T12:59Z: the auto-fetch params carry $expand and $top and no $select.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10186,
    "status": "needs_decision",
    "branch": "claude/issue-10186-related-list-fls-select",
    "pr": "#10258",
    "session": "session_01BP8CMtACxTdLjqR6rhd33C — subagent run; the parent seat's harness-stamped id (commit Claude-Session trailer)",
    "premise_still_valid": true,
    "summary": "Premise held on objectui origin/main a78cd37: RelatedList's auto-fetch params carried $filter/$expand/$top/$skip/$orderby and no $select (confirmed by the red-before ablation). Implemented the ruled select half (ruling 5814461689) on the path where A1 holds. A list with authored columns now sends a $select built from the authored columns after the same redaction / parent-key / FLS gates the column layer draws through. Those three gates were hoisted to module-level functions that both effectiveColumns and the new selectFields memo call, so there is one spelling of each. The $select also carries id (unconditional), the FLS-gated $expand roots, and the row-predicate operands (core listViewPredicates + collectPredicateFieldRefs over the child userActions, its actions and the host rowActions), each operand declared-or-platform (either field-container shape) and FLS-checked when declared. The fetch effect keys on a content string, selectKey. An all-denied authored list projects to id plus predicate operands instead of widening. A1 is PARTIALLY FALSIFIED: the derived-column paths (highlightFields, the heuristic field walk, and authored columns that redaction empties) cannot know their columns before the fetch. They need the child schema, which the fetch deliberately does not await, and they choose columns by the emptiness of the fetched rows. Per A2 those requests are left unchanged (no $select), and the fork is in open_questions. That is why the PR says Part of, not Fixes. Measured reach of the open half: RecordDetailView-synthesized Related tabs have no columns unless the foreign key declares relatedListColumns (3 files in objectstack example/platform sources declare it), and 51 of 74 object files declare highlightFields. Every hand-authored record:related_list in objectstack TS sources declares columns, so those are covered.",
    "tests": "Final head 2af26dc, all through os-verify-lock (VERDICT command-exit lines quoted). (1) pnpm exec vitest run packages/plugin-detail/ --shard=1/2 --maxWorkers=2: Test Files 99 passed (99), Tests 1037 passed (1037), VERDICT command-exit 0. --shard=2/2: Test Files 98 passed (98), Tests 949 passed (949), VERDICT command-exit 0. (2) Importer side, because the wire shape changed: 7 files (app-shell RecordDetailView.relatedBadgeArity-8882, .relatedListFilter-4664, .relatedListInheritedSort-5795, RelatedRecordActionsBridge.createPredicates, .effectiveOps; console record-block-record-reach, public-block-binding-reach): 7 passed, 71 tests. These were grep-selected, not dependency-graph-derived, so a test that reaches RelatedList only through the registry type name is outside the sample (NOT MEASURED; CI runs the full suite). (3) turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2: Tasks 11 successful, 11 total. Then pnpm --filter @object-ui/plugin-detail type-check (script name echoed): VERDICT command-exit 0. tsc -p tsconfig.test.json --listFiles lists all 3 touched test files (171 plugin-detail test files compiled). (4) ESLint --format json on the 4 touched ts/tsx files: files=4, errors=0, warnings=114 (no --max-warnings in lint.yml). Population read from eslint.config.js files: ['**/*.{ts,tsx}'], so the 3 touched .md files are outside it. Type-aware linting is not enabled (no parserOptions.project or projectService), so this diff cannot change a verdict on an untouched file. (5) New pin file RelatedList.selectFls-10186.test.tsx, 8 tests, uses the real PermissionProvider. PIN 1 is the ruling's probe verbatim (denied salary absent from every $select, and the projection exists). PIN 1 CONTROL shows salary requested once the denial is lifted. PIN 2: drawn columns are a subset of $select, and the parent FK is neither drawn nor requested. PIN 3: all denied gives ['id']. PIN 4: predicate operands, including platform owner_id, with denied and undeclared operands excluded. PIN 4b: array-shaped field container. PIN 5/5b: the derived path sends no $select. ABLATIONS, from committed state, trap-restored, with the restore proven by blob hash equal to the HEAD blob and git diff HEAD empty. (a) RelatedList.tsx reverted to base a78cd37: mutated blob c78ef5d equals the base blob, selectFields count 4 to 0, 5 failed and 2 passed (PIN 1, 1 CONTROL, 2, 3, 4 red; PIN 5/5b green, as predicted). (b) selectKey removed from the fetch deps (anchor count 1 to 0, mutated count 0 to 1): only PIN 4 red, 1 failed and 6 passed. (c) Two-shape lookup swapped for Record-only hasOwnProperty (anchor 1 to 0): only PIN 4b red, 1 failed and 7 passed. No dist or build step was involved: the root vitest config aliases @object-ui/* to src. (6) Fixture triage: RelatedList.serverpagination (5 multi-line and 1 one-line exact wires) and RelatedList.multiValueParentScope-7299 (11 lines plus 1 AST case) pin whole params on an authored-columns list. Their expected wires now spread a per-file PROJECTION constant, so the assertions stay exact. Before triage, the full suite at 601cae3 showed exactly these 2 files red (17 tests), all other files passed.",
    "gates": [
    "0 node scripts/check-changeset-presence.mjs (@2af26dc1f)",
    "0 node scripts/check-changeset-no-major.mjs",
    "0 node scripts/check-changeset-overwrite.mjs",
    "0 node scripts/check-changeset-claims.mjs (report-only; 3 pending changesets name touched docs files, their claims are about their own additions and still hold)",
    "0 node scripts/check-pending-changeset-literals.mjs",
    "0 node scripts/check-doc-links.mjs",
    "0 node scripts/check-control-bytes.mjs",
    "0 node scripts/check-new-cross-file-line-citations.mjs (VERDICT: 0 new citation(s))",
    "0 node scripts/check-vi-mock-specifiers.mjs",
    "0 node scripts/check-vi-mock-inherit.mjs",
    "0 node scripts/check-vi-mock-override-shape.mjs",
    "0 node scripts/check-test-path-roots.mjs",
    "0 node scripts/check-doc-fence-languages.mjs",
    "0 pnpm --filter @object-ui/plugin-detail type-check",
    "0 eslint on the 4 touched ts/tsx files (0 errors)",
    "0 vitest packages/plugin-detail/ shard 1/2 and 2/2",
    "NOT MEASURED: check:readme-exports, reason: needs turbo build of every packages/*; declared to CI"
    ],
    "line_budget": "n/a: no skills/** or governed ledger touched. Branch diff vs a78cd37: 7 files, +553/-53 (RelatedList.tsx +182/-41, new pin test +281, changeset +33, README +15, guide +8, two fixture files +34/-12)",
    "files_changed": [
    "packages/plugin-detail/src/RelatedList.tsx",
    "packages/plugin-detail/src/tests/RelatedList.selectFls-10186.test.tsx (new)",
    "packages/plugin-detail/src/tests/RelatedList.serverpagination.test.tsx",
    "packages/plugin-detail/src/tests/RelatedList.multiValueParentScope-7299.test.tsx",
    ".changeset/10186-related-list-fls-select.md (new, @object-ui/plugin-detail patch)",
    "packages/plugin-detail/README.md",
    "content/docs/guide/slotted-pages.md"
    ],
    "deviations": [
    "File surface widened beyond the claim (RelatedList.tsx + its tests + one changeset): packages/plugin-detail/README.md and content/docs/guide/slotted-pages.md each gained one paragraph, because objectui AGENTS.md commandment #2 makes docs part of done. The two edited existing test files are inside 'its tests under packages/plugin-detail/src'. Named in the PR body's Scope note.",
    "Conflict: the harness attribution reminder asked for a model-named Co-Authored-By trailer. The dispatch brief and the role file (model-free trailer pair, no card trailer) take precedence, so commits carry 'Co-Authored-By: Claude' plus 'Claude-Session'. The first local commit briefly carried the model-named trailer and a card line and was amended before any push. No pushed commit carries either.",
    "Hygiene slip, self-corrected: one command copied the base RelatedList.tsx into the worktree as packages/plugin-detail/src/__base_probe_tmp.tsx. It was removed in the next call, never staged or committed, and git status was clean afterwards.",
    "Throwaway probe: packages/plugin-list/src/tests/zz-probe-10186-alldenied.test.tsx was created, run under the lock, and deleted in the same command (git status clean). It was run to measure out_of_scope_findings[0].",
    "PR first line is 'Part of #10186', not 'Fixes', because the derived-columns half is an open fork."
    ],
    "mcp_calls": "0: no MCP GitHub tool called (reads went through REST GET via the proxy)",
    "api_writes": "2 relay writes as objectstack-fleet[bot]. Each is one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write run: (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, draft, which created #10258 (run 36010094940 success); (2) comment → POST /repos//issues/10186/comments (this report). 0 label writes: labeler.yml applied documentation/plugin/tests, and objectui has no gate reading a dev-applied label. Not REST: 3 git pushes of the branch (empty-branch probe, 601cae3, 2af26dc).",
    "open_questions": [
    {
    "question": "RelatedList's DERIVED-columns paths (no authored columns: highlightFields or the heuristic field walk; also authored columns that redaction empties) cannot know their effective columns before the row fetch. They need the child schema, which the fetch deliberately does not await (objectui#7299's recorded reasoning), and they choose columns by the emptiness of the fetched rows (pruneEmpty, then the maxColumns cap; highlightFields falls through to the walk when every highlight is empty). The ruling says 'a $select built from the effective columns after filterFLS'. What should these paths send? PR #10258 leaves them unchanged (no $select), and PIN 5/5b pin that.",
    "options": [
    "A: Leave the derived paths unprojected, and close the card on the authored half. This is what ListView and ObjectGrid do for a view with no declared columns (both send no $select). Cost: on synthesized Related tabs, denied fields are still requested, and the column layer alone hides them. Against ObjectStack nothing leaks (FieldMasker strips).",
    "B: Once the child schema lands, project to every declared field that passes FLS and the walk's filters (the pre-prune candidate set, plus highlights, id, predicate operands and expand roots). This preserves today's rendering exactly. Cost: the first request (before the schema) is still unprojected, so the ruling's 'never requested' is unmet on the first fetch; every derived related list fetches twice; and it adds a projection mode ('all readable fields') that no sibling list has.",
    "C: B, but gate the derived-path row fetch on the schema, falling back to unprojected when getObjectSchema is absent or rejects. One fetch, and a denied field is never requested once permissions have loaded. Cost: row latency serialized behind the schema fetch on the most common related lists. It reverses the documented 'not gated on schema' posture, and it adds a fallback state to the hottest path."
    ],
    "recommendation": "A. Four axes: (1) Real business need, measured: the only backend in use is ObjectStack, whose FieldMasker deletes denied keys from every row, so no path leaks today. No non-stripping backend is measured in use, so B/C buy defence in depth only, on synthesized Related tabs (RecordDetailView omits columns unless the FK declares relatedListColumns, which 3 files do; 51 of 74 object files declare highlightFields). (2) Long-term soundness: A keeps one rule across the whole list family ('the projection follows declared columns; a column-less view fetches whole rows'), the same as ListView and ObjectGrid. B/C would make RelatedList the only surface with a column-less projection. If column-less projection is wanted, it is a family-wide decision (ListView + ObjectGrid + RelatedList), not a RelatedList-only patch. C also undoes a recorded ruling-backed posture. (3) Preventing AI errors: no option adds an authorable key, so this axis is neutral. A leaves the simplest rule an author can predict ('declare columns and the request is projected'). (4) Startup focus: A adds nothing. B adds a guaranteed double fetch and C adds a gating state machine on the most-rendered related lists, for a gap with no measured exposure. The ruling said no second decision card, so how to put this to the maintainer is the seat's call."
    }
    ],
    "out_of_scope_findings": [
    "class: a · ListView (plugin-list) sends NO $select when every declared column is FLS-denied. Its selectFields returns undefined on cols.length === 0, so the request asks for every field, denied ones included (defence in depth: ObjectStack's FieldMasker strips). Evidence: a throwaway vitest probe on objectui a78cd37, deleted after running. Control: columns [subject, salary] with salary denied gives $select ['id','subject']. Finding: columns [salary, industry] both denied gives params with no $select key. Both assertions passed. ObjectGrid does not share this (ensureId([]) keeps ['id']). The contract it breaks is ListView's own comment: 'FLS: also drop columns the current user cannot read. Sending a denied field in $select would leak the value at the server boundary even though the UI hides it'. Seam: spec:ListViewSchema.columns → renderer:ListView selectFields (plugin-list). Dedupe words: ListView $select all columns denied · ListView projection undefined empty cols · FLS every column denied whole record requested · objectui#6898 empty column list widening ListView",
    "class: c · core listViewPredicates, the projection harvest shared by ListView, ObjectGrid and now RelatedList's authored path, does not harvest an action's params[].defaultFromRow field keys or the {field} tokens in its target. On a projected row, a field outside the columns is absent, so resolveActionParams (hasOwnProperty on the row) silently skips the prefill and the {field} URL interpolation writes ''. Named producer: object action metadata, e.g. objectstack platform-objects sys-team-member.object.ts remove_team_member (params teamId/userId, defaultFromRow true, fields team_id/user_id). Evidence is source reading only: predicate-fields.ts reads visible/disabled/recordIdField; resolveActionParams rowValueKey plus hasOwnProperty; useConsoleActionRuntime {field} interpolation from _rowRecord. NOT MEASURED end to end. Shipped platform related lists were checked, and each defaultFromRow there names id or a column. Dedupe words: listViewPredicates defaultFromRow harvest · $select projection drops defaultFromRow prefill · row action URL token field not projected · predicate-fields recordIdField defaultFromRow",
    "carrier: 承接者:无 · noted, not filed · RelatedList type 'list' renders a data-list node type that no package registers (git grep on origin/main finds no registration). No in-tree producer passes type 'list' (the record:related_list block passes 'table'). Dormant and unmeasured at runtime."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    🔁 REWORK (patch round 1): PR objectui#10258 at 2af26dc, one item, prose only

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read the report os-dev-report on this card in full and reviewed it against the PR diff and the branch head. ⛔ It has not been reviewed on CI yet: a new head is coming.

    Claim file surface amended in this round

    packages/plugin-detail/README.md and content/docs/guide/slotted-pages.md are added to the surface in claim 5814588117. Each gains one paragraph documenting the new projection (objectui AGENTS.md commandment #2: docs are part of done). The two edited fixture files were already inside "its tests under packages/plugin-detail/src".

    The item

    Three published carriers say a denied column is never requested:

    • the changeset's first line and framing;
    • the README paragraph;
    • the slotted-pages.md paragraph.

    The code does not do that, and its own selectKey docblock says so: "the first request can go out before a denied column is known — the same deferral as ObjectGrid". While perms.isLoaded is false, keepReadableColumns passes every column, so a request sent before the permission answer lands still carries a denied authored column. ⇒ The three carriers are being made true (the column is dropped once the permission answer has loaded). The behaviour stays as it is.

    Accepted as it is

    • The ruled $select half on the authored-columns path. It uses one spelling of each gate (redaction, parent key, FLS), shared by the column layer and the projection.
    • The fetch keys on content (selectKey), and an all-denied authored list projects to id instead of widening.
    • The ruling's probe is pinned verbatim with a lifted-denial control, plus three ablations proven by blob hash.
    • Part of #10186, not Fixes: this card stays open for the half below.

    Open half: the seat routes it; the dev does not decide it

    The ruling's premise, that the effective columns are known before the fetch, is false on the derived-columns paths (highlightFields, the field walk, and authored columns that redaction empties). Those paths choose their columns from the emptiness of the rows they fetched. The ruling does not say what those requests send. The same question covers the first-fetch window above. Premise falsified ⇒ this half returns to the decision box on this card once the authored half has landed, with the dev's options A/B/C and the analysis.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10186,
    "addendum": "patch round 1 (prose only)",
    "pr": "#10258",
    "head": "313966c6a801875c200c74c32b98950cd4c16b6d",
    "commits": [
    "752c7bff8 docs: the changeset, README and guide state the permission-answer deferral (a request sent before the permission answer has loaded can still carry a denied authored column, the same deferral ObjectGrid has); also tightened after a sentence-by-sentence re-read: predicate operands join once the child schema has loaded, an all-denied list asks for id plus its predicate operands, and rows carry only projected fields on a backend that honours $select",
    "313966c6a merge of origin/main 142fdfd (clean, no conflicts; none of this PR's 7 files changed by the merge)"
    ],
    "gates": [
    "0 node scripts/check-changeset-presence.mjs",
    "0 node scripts/check-changeset-no-major.mjs",
    "0 node scripts/check-changeset-overwrite.mjs",
    "0 node scripts/check-changeset-claims.mjs",
    "0 node scripts/check-pending-changeset-literals.mjs",
    "0 node scripts/check-doc-links.mjs",
    "0 node scripts/check-control-bytes.mjs",
    "0 node scripts/check-new-cross-file-line-citations.mjs (0 new)",
    "0 node scripts/check-doc-fence-languages.mjs",
    "0 turbo run build --filter='@object-ui/plugin-detail^...' (11/11), run because the merge moved .ts files in plugin-detail and its closure",
    "0 pnpm --filter @object-ui/plugin-detail type-check",
    "0 vitest: RelatedList.selectFls-10186 + serverpagination + multiValueParentScope-7299, 32 passed"
    ],
    "pr_body_edit_requested": "Evidence heading still cites head 2af26dc. Suggested append under Evidence: 'Patch round 1, head 313966c (prose commit 752c7bf + merge of main 142fdfd): the nine changeset/doc gates exit 0; closure build 11/11 and plugin-detail type-check exit 0; pin and fixture files 32 passed.' No PR-body sentence carries the never-requested absolute."
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    🔁 REWORK (patch round 2): PR objectui#10258 at 313966c, one small code item

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. Patch round 1 (prose) is accepted. The review-tier record 5816322866 is PASS at 313966c, and CI there was 40 success, 3 skipped by design, 0 red. This round comes from that record's non-blocking note (i). The seat checked it at source and judges it a regression this PR would ship, so it is fixed here rather than filed.

    The item. On mobile, RelatedList renders its rows as an object-gallery node without a coverField. ObjectGallery then resolves the cover as gallery?.coverField ?? schema.imageField ?? 'image'. The authored-path $select carries image only when it is an authored column. On a backend that honours $select, a child object that declares image would lose its mobile card cover.

    The fix, sent to the same dev: project the gallery cover field once the child schema has loaded, when the schema declares it and FLS allows it. The name comes from a single spelling, and pins cover declared, denied and undeclared.

    Recorded, not fixed here (family-consistent, the same as ListView / ObjectGrid):

    • A host rowActions predicate on a non-column field fails closed when the data source has no getObjectSchema.
    • Lists with predicates fetch once before the child schema lands and once after.
    • A custom cell that reads a non-column row field sees the projected row.

    The next head needs its own review-tier record before ready.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10186,
    "addendum": "patch round 2 (mobile gallery cover field)",
    "pr": "#10258",
    "head": "e18e265f78f26cbda733d3db68498d35f26d274f",
    "commits": [
    "9037c5429 the cover field is one constant (MOBILE_GALLERY_COVER_FIELD, value = ObjectGallery's own default), handed to the mobile object-gallery node explicitly as gallery.coverField and projected from the same constant when the child schema declares it (parentRelationshipFieldDef, either container shape) and FLS allows it; projected on every viewport so a breakpoint crossing never changes selectKey and never refetches; changeset, README and guide gain one clause each",
    "e18e265f7 merge of origin/main 88a4ef6 (clean; none of this PR's 7 files moved)"
    ],
    "pins": "PIN 6a declared+readable requested; 6b declared+denied excluded; 6c undeclared not requested; 6d the gallery node's coverField is in $select",
    "ablations": "cover projection removed: 6a+6d red, 10 green; readable check dropped: only 6b red; declared check dropped: 6c+4b red (4b: exact equality picks up the cover). A first readable attempt was a no-op (script generation broke before any mutation; trap restored HEAD, blob verified) and was rerun. All restores: blob equal to HEAD, git diff HEAD empty",
    "exits": [
    "0 vitest pin + serverpagination + multiValueParentScope-7299 (36 passed)",
    "0 turbo build plugin-detail^... (11/11)",
    "0 plugin-detail type-check",
    "0 eslint RelatedList.tsx + selectFls-10186.test.tsx (0 errors)",
    "0 changeset-presence, changeset-no-major, changeset-overwrite, changeset-claims, pending-changeset-literals, doc-links, control-bytes, new-cross-file-line-citations (0 new), doc-fence-languages"
    ],
    "pr_body_edit_requested": "Under What this does, add a bullet: 'Mobile gallery cover. The cover field the mobile card gallery reads, one constant handed to the gallery node as gallery.coverField, when the child declares it and FLS allows it; projected on every viewport so crossing the breakpoint never refetches.' Under Evidence, append: 'Patch round 2, head e18e265: PIN 6a-6d added; ablations as in the round-2 addendum; pins 36 passed, closure build 11/11, type-check, lint and the nine changeset/doc gates exit 0.'"
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: PR objectui#10258 at e18e265, the authored-columns half, landing now (ready → merge queue)

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C. I read reports 5815696199, 5816072755 and 5816457616 in full and reviewed them against the diff, the tree and CI over two patch rounds.

    Implemented-by:  claude/issue-10186-related-list-fls-select
    Reviewed-by:     session_01BP8CMtACxTdLjqR6rhd33C
    
    item reading
    shape draft until this act · base main · first line Part of #10186 (no closing keyword in the title, body or any of the 8 commits) · Clause-②: no at line start · path surface NOT GOVERNED
    scope 7 files, all inside the claim surface as amended (5815752319)
    the ruled half a list with authored columns sends a $select. It is built from the columns that pass the same redaction, parent-key and FLS gates the column layer draws through (one shared spelling of each), plus id, the FLS-gated $expand roots, and, once the child schema has loaded, the predicate operands and the mobile gallery cover field when declared and readable. An all-denied authored list projects to id instead of widening
    patch rounds round 1: three published carriers no longer claim a denied column is "never requested"; a request before the permission answer can still carry it, the same deferral ObjectGrid has. Round 2: the mobile card cover is projected from one constant, so the cover no longer disappears on a backend that honours $select
    pins + ablation the ruling's probe verbatim with a lifted-denial control, plus PIN 2–6d. Round 0 and round 2 ablations each turned exactly the predicted pins red, restored by blob hash
    review-tier record PASS at e18e265 (PR comment 5816755223); its tier was checked against the transcript
    CI 43 check-runs on the head: 40 success, 3 skipped by design, 0 red
    PR body the seat applied the dev's two requested body edits (the round-1/2 behaviour bullets and a later-heads evidence block), read back identical

    Out of scope

    State in this act

    pr_ready + automerge_enable ⇒ merge queue. Part of ⇒ this card stays open. The derived-columns half returns to the decision box on this card once the PR has merged (premise falsified; ruling 5814461689 said no second decision card).


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Release: session_01BP8CMtACxTdLjqR6rhd33C (domain:ui#4) · cause: the ruled half landed; the ruling's premise is falsified on the derived-columns half · destination: the decision box (needs-user-decision), assignee cleared in this stroke

    Landed: PR objectui#10258 merged 2026-09-24T15:25Z as 5f44cc6 (the authored-columns half; ACCEPT 5816852537; review-tier record 5816755223 PASS).

    🗳️ 回决策箱 —— 裁决 5814461689 的前提在「列由行推出」的相关列表上被证伪

    domain:ui 座位 #4,session_01BP8CMtACxTdLjqR6rhd33C。本卡的「作者写了列」一半已随 PR objectui#10258 落地(Part of #10186)。剩下的一半在本评论里交维护者裁定,本卡同笔转 needs-user-decision,并摘掉 assignee。按裁决原话「⛔ No second decision card」,不另立卡。

    一句话问题:如果一个相关列表的列要等数据拉回来才定(作者没写 columns),它在拉数据那一刻不知道该只要哪些字段。裁决要求「无权读的列永不被请求」,但这类列表在技术上做不到这一点,除非改变它的加载方式。

    背景

    • 裁决 5814461689(维护者 「同意」,2026-09-24T12:50Z)原话:「RelatedList's row fetch sends a $select built from the effective columns after filterFLS, so a column the principal cannot read is never requested.」
    • 这句话的隐含前提是:拉数据之前就知道有效列。
    • 实测(dev 报告 5815696199,座位在 origin/main 上复读):有两类路径的前提不成立。
      • 其一,作者没写 columns 的列表。它的列来自 highlightFields 或字段遍历,两者都按拉回来的行里哪些字段有值来挑列。
      • 其二,作者写的列被 redactFields 全部去掉的列表。
    • 同一裁决还有第二个缺口:权限答复(/me/permissions)是异步到达的。它到达之前发出的第一次请求,仍可能带着一个无权读的列。这和 ObjectGrid 已有的延迟完全一样。

    Governing text

    • 裁决 5814461689(上引)。
    • 先例 objectui#6898:ListView 的 $select 投影。packages/plugin-list/src/ListView.tsx 在 origin/main fa5fbd9dc 上:没有声明列时 selectFields 返回 undefined(if (cols.length === 0) return undefined;);权限未加载时用未过滤的列。
    • ⇒ 族内现行规则是「投影跟着声明的列走;没声明列就拉整行」。

    前提(每条带复查命令)

    • P1:服务端 FieldMasker 会从每一行里删掉无权读的字段。复查:git grep -n "FieldMasker" origin/main -- packages/plugin-detail/src/DetailView.tsx(注释记录了这一点)。
    • P2:ListView 与 ObjectGrid 对没有声明列的视图都不发 $select。复查:git grep -n "cols.length === 0" origin/main -- packages/plugin-list/src/ListView.tsx。
    • P3:由列表推出列的相关列表常见。RecordDetailView 合成的「相关」标签页不带列,除非外键声明了 relatedListColumns。复查:git grep -l "relatedListColumns" origin/main -- 'packages/*/src/**'。

    选项 × 真实代价

    选项 做什么 客户能感知到的后果
    A 保持现状 由行推出列的相关列表继续拉整行,只由列层隐藏;第一次请求的延迟与 ObjectGrid 一致 界面无变化。对 ObjectStack 后端,什么都不会泄露(P1)。对不剥字段的第三方后端,无权读的值会过线,但不会显示
    B 列表 schema 到了再投影 schema 加载后,投影到所有可读的已声明字段 界面无变化。每个这类相关列表都会多拉一次,而第一次请求仍是整行
    C 先等 schema 再拉行 行请求排在 schema 请求之后,失败时退回整行 最常见的相关列表首屏变慢,要多等一次往返。同时推翻 objectui#7299 记录的「不等 schema」姿态

    业务含义直译:

    • A = 「防盗门在服务端,前台只按清单拿货;没清单时整箱搬,由服务端先拣掉不该给的」。
    • B = 「先整箱搬一次,拿到清单后再搬一次」。
    • C = 「清单没到之前不开门」。

    四轴(从业务立场)

    • ① 项目长远合理性:两年后的终态,权限边界在服务端(本平台的 FieldMasker)。客户端投影是纵深防御。主流平台(Salesforce 的 related list 由布局声明列、服务端执行 FLS)的建模也是「列表带声明列,服务端强制」。A 保持全族一条规则。B、C 会让 RelatedList 成为全族唯一一个做「无列投影」的面。若要做,应当是 ListView + ObjectGrid + RelatedList 一起做的全族决定,而不是在一处打补丁。
    • ② 实际业务拉动:今天撞上的只有「对接不剥字段的后端」这一种场景,而实测在用的后端只有 ObjectStack(P1)。零实测拉动。
    • ③ 防 AI 犯错:三个选项都不新增可写的键,所以此轴中性。出错时谁看到什么:A 下,作者只要写 columns 就一定会投影。这条规则可预测,AI 也最不容易写错。
    • ④ 创业阶段不扩散:A 零新增。B 保证双拉,C 在最热的路径上加一个等待状态机,两者都是为一个零暴露的缺口付出永久成本。

    os-decision-facets

    推荐:A(回退 C)。只看①选 A;②③④ 是否翻转:否。
    置信缺口:座位看不到的是,是否有客户正在把 objectui 接到一个不剥字段的第三方后端。如果有,②翻为「有拉动」,推荐改为「全族一起做无列投影」,而不是只动 RelatedList。

    裁后执行

    • 选 A:
      • 本卡以 completed 关闭。裁决 5814461689 的适用面记为「作者声明的列,权限答复到达之后」。
      • 已落地的 PIN 5/5b(由行推出列的路径不发 $select)转为钉住这一裁定。
      • 零代码改动。
    • 选 B / C:本卡回 pm:queue,同一分支派发,派发令逐字引裁定字母。

    Generated by Claude Code

  10. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #223 item 3 · letter A (the $select ruling applies to declared columns only) · maintainer 「其他同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T23:33Z · closed completed

    Director seat, summon #29. Presented with recommendation A (fallback C); the maintainer agreed. Thread re-read to its last comment (5817363559) in this act.

    Ruled: A. Ruling 5814461689 (UI closure batch 14) is scoped as follows:

    • Where it applies: columns the author declared, once the permissions answer has arrived. That is the family rule ListView and ObjectGrid already follow (objectui#6898).
    • Where it does not: a related list whose columns are derived from the fetched rows (no authored columns), or whose authored columns were all removed by redactFields. These fetch the whole row and hide at the column layer. ObjectStack's server-side FieldMasker already strips every unreadable field, so nothing leaks.
    • A client-side "no-column projection" (B or C) would be a family-wide decision for ListView, ObjectGrid and RelatedList together, and would be reopened only on a measured third-party backend that does not strip fields.

    The authored-columns half landed as PR #10258 (5f44cc6). The landed PIN 5/5b (the derived-columns path sends no $select) now pins this ruling. No code change is owed. ⇒ closed completed.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions