Repository navigation
finding(plugin-detail): record:related_list honours redactFields / enforceFieldSecurity — two keys its own spec contract (RecordRelatedListProps, a strictObject) rejects #9095
Description
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.parseand it is rejected,
becausestrictObjectrefuses 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.
are open and this is not the seat that picks:
- A — declare them. Add both keys to
RecordRelatedListProps(and to the
block'sinputs), making the authoring surface match what the renderer honours.
Costs a spec change inobjectstack; 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
os-litant commentedon Sep 11, 2026
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 atorigin/main154fe2aby the TypeScript checker, ⛔ never a grep:⇒ ⭐ 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 anyerases 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:
redactFieldsdown intoRelatedListso 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 becauseschema = {} as anyerases 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 becauseschema = {} as anyerases a correct annotation (2 of 7, objectui#8327 class (a)) #8649 was filed 2026-09-08 and could not see it.RecordRelatedListRenderer.columnMembers.test.tsxcalls 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.RecordRelatedListProps.parseand it is rejected, because the contract is astrictObject. ⇒ an author who writesredactFieldsgets 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
objectuiboardredactFieldsenforceFieldSecurityrecord:related_listplugin-detail(control)The family, and why only one of them is the duplicate:
schema = {} as anyerases a correct annotation (2 of 7, objectui#8327 class (a)) #8649 — the superset. This card closes into it.pm:dispatched, p2,security) — redaction defeated when every authored column is redacted. A behaviour defect; its PR is what widens the reach above. ⛔ Different card.pm:queue, p1,security) and record:related_list: 身份解析不出的列条目绕过 enforceFieldSecurity / redactFields,然后经表格自己的 accessorKey 照常渲染 #8793 (pm:dispatched, p1) — the fail-open-on-unresolvable-identity defect onrecord:detailsandrecord:related_list. ⛔ Different defect: those are about what the fold does, this is about whether the keys are declared.pm:seatpost.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+176 · 2026-09-11T01:1xZ · 本评论来自分诊座位Generated by Claude Code
schema = {} as anyerases a correct annotation (2 of 7, objectui#8327 class (a)) #8649