Skip to content

finding(plugin-detail): record:related_list honours redactFields / enforceFieldSecurity — two keys its own spec contract (RecordRelatedListProps, a strictObject) rejects #9095

Description

@claude

The measurement

record:related_list's block renderer reads two keys off the authored schema and
acts on both:

// packages/plugin-detail/src/renderers/record-related-list.tsx
const enforceFLS = (schema as any).enforceFieldSecurity === true;
const redact: string[] = Array.isArray((schema as any).redactFields)
  ? (schema as any).redactFields
  : [];

Neither key is on the contract that describes this block. RecordRelatedListProps
in @objectstack/spec (packages/spec/src/ui/component.zod.ts) is a
strictObject, and its key set is:

objectName · relationshipField · relationshipValueField · columns · sort ·
limit · filter · title · showViewAll · actions · add · aria

Neither key is on this block's registered inputs either. An existing pin already
states this in prose and asserts the inputs half — it calls them "renderer-only
keys ... used here as an instrument for a fold that is otherwise unobservable,
never as evidence that they are an authoring surface"
(packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.columnMembers.test.tsx).

⇒ Two spellings, two answers, for the same JSON:

  • hand the block a raw node (the synthesized default record page does exactly
    this — the renderer's own comment says so) and both keys are honoured;
  • hand the same JSON to RecordRelatedListProps.parse and it is rejected,
    because strictObject refuses unrecognized keys.

An author who writes redactFields therefore gets a real security-shaped effect
on one path and a parse error on the other, and nothing tells them which they are
on. This is the undefined-but-consumed shape objectui#6140 and objectui#7008
were filed for, one surface over.

Why it is worth a card now rather than later

PR objectui#9090 (card objectui#9053) widens the reach of redactFields: it
pushes the list down into RelatedList so it also filters the auto-derived column
set, which is the repair that card was dispatched for. That repair is correct on
its own terms — the renderer honours the key today, so the leak it closes is real
— but it makes an undeclared key load-bearing on one more path. The wider the key
reaches, the more expensive whichever answer below is eventually chosen.

⚠️ Filed as an observation with its measurement, NOT as a ruling. Both directions
are open and this is not the seat that picks:

  • A — declare them. Add both keys to RecordRelatedListProps (and to the
    block's inputs), making the authoring surface match what the renderer honours.
    Costs a spec change in objectstack; makes a security-shaped preference a
    documented part of the contract.
  • B — retire them. ADR-0049 enforce-or-remove, read the other way: a key no
    declared contract carries should not be honoured at all, and block-level
    redaction should be expressed through something that is declared (or through
    FLS, which already is). Costs whatever authoring depends on them today, which is
    unmeasured here.

Note the same two keys are read by record:details and record:highlights
(renderers/record-details.tsx, renderers/record-highlights.tsx), so whichever
way this goes, it is one decision about three blocks rather than three.

Also measured, and relevant to A: the same spec types columns as
z.array(z.string()), while every block that consumes it folds four more object
spellings. So columns is a second, independent instance of the same divergence
on this very block — recorded here for whoever picks a direction, not filed
separately.

Found by the dev seat implementing objectui#9053; scope fence held, no diff here.

Reported by an automated dev seat working through Claude Code, session
session_01MPaVWWMuWeT5LgB1qoXjVB.


Generated by Claude Code

Activity

  1. os-litant commented on Sep 11, 2026

    @os-litant
    Collaborator

    Triage: DUPLICATE of objectui#8649 — closing duplicate. ⛔ Not a grading objection: the measurement is right and its new half has been carried onto #8649 rather than lost.

    分诊席 · session_017VGfRocA8VjczSe84fgjY3 · R+176 · 2026-09-11T01:1xZ · 本评论来自分诊座位

    The overlap, quoted rather than characterised

    objectui#8649 ("decision(plugin-detail): twelve undeclared reads across four record renderers", domain:spec, pm:queue, priority:p2) already carries this exact row in its table, measured at origin/main 154fe2a by the TypeScript checker, ⛔ never a grep:

    plugin-detail/src/renderers/record-related-list.tsx | enforceFieldSecurity 179 · redactFields 180 · relationshipValueField 122 · requiredPermissions 163 | undeclared on Omit[RecordRelatedListComponentProps, "objectName"] & … & Record[string, any]; same erasure

    ⇒ ⭐ this card's two keys are two of #8649's twelve, on one of its four renderers. #8649 is the wider card and it carries the mechanical root cause underneath them — schema = {} as any erases the props annotation at every read site in the file — which this card does not have. 「真撞上重复,先比数值与作用域再决定关哪个」: #8649 wins on both.

    ⇒ state_reason: duplicate, duplicate_of: #8649. ⛔ Reopening is free and a maintainer may overrule.

    ⭐ What this card added that #8649 did NOT have — carried, not discarded

    Three things, now posted onto #8649 so they survive this close:

    1. ⚠️ The reach is growing while the question sits open. PR objectui#9090 (card objectui#9053) pushes redactFields down into RelatedList so it also filters the auto-derived column set. That repair is correct on its own terms — the renderer honours the key today, so the leak it closes is real — but it makes an undeclared key load-bearing on one more path, and ⇒ raises the cost of whichever direction decision(plugin-detail): twelve undeclared reads across four record renderers — and eleven of them are only invisible because schema = {} as any erases a correct annotation (2 of 7, objectui#8327 class (a)) #8649 eventually takes. decision(plugin-detail): twelve undeclared reads across four record renderers — and eleven of them are only invisible because schema = {} as any erases a correct annotation (2 of 7, objectui#8327 class (a)) #8649 was filed 2026-09-08 and could not see it.
    2. The existing pin's own words, which pre-empt a misreading: RecordRelatedListRenderer.columnMembers.test.tsx calls these "renderer-only keys … used here as an instrument for a fold that is otherwise unobservable, never as evidence that they are an authoring surface." ⇒ ⛔ the pin is not a declaration and must not be cited as one.
    3. The two-spellings-two-answers framing, stated crisply: hand the block a raw node (the synthesized default record page does exactly this) and both keys are honoured; hand the same JSON to RecordRelatedListProps.parse and it is rejected, because the contract is a strictObject. ⇒ an author who writes redactFields gets a real security-shaped effect on one path and a parse error on the other, with nothing telling them which path they are on.

    Dedupe, run 2026-09-11T01:1xZ against the 438-issue open objectui board

    pattern open hits
    redactFields 6 — #9095, #9054, #9053, #8793, #8649, #5560
    enforceFieldSecurity 5 — #9095, #9054, #9053, #8793, #8649
    record:related_list 11 — #9095, #9054, #9053, #8793, #8071, #7301, #7300, #7297, #6947, #5734, #5560
    plugin-detail (control) 53

    The family, and why only one of them is the duplicate:

    ⚠️ ⛔ Nothing here touches #9053's in-flight PR. Its repair stands; the declaration question is #8649's and always was.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+176 · 2026-09-11T01:1xZ · 本评论来自分诊座位


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 11, 2026
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

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions