Skip to content

fix(plugin-detail): fail closed when a related-list column has no resolvable identity - #9058

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-8793-related-list-identity-fail-closed
Draft

fix(plugin-detail): fail closed when a related-list column has no resolvable identity#9058
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-8793-related-list-identity-fail-closed

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #8793

record:related_list's field-security fold kept any column entry whose identity
it could not resolve. That entry then rendered its real value through the table
library's own key. The else-branch now excludes it.

⚠️ The hard-stop verdict, first

The dispatch's stop condition was: does the measured blast radius include
columns that are legitimately visible today and simply authored in the
accessorKey spelling?

Measured answer: no. Zero columns in this repository stop rendering. The
measurement is below, with its commands and a lit positive control. The stop did
not fire.

But the same measurement turned up something louder, and it is not the thing the
stop was watching for — see "The second hole" below. It is filed, not patched.

The defect

Two read points resolve a related-list column's identity and they disagree; the
security filter uses only one of them.

  • The block's own fold resolves through columnIdentity, which deliberately
    refuses accessorKey (TABLE_ADAPTER_COLUMN_KEY, objectui#3104 — it is
    TanStack Table's column key, not ObjectStack metadata identity), plus a key
    tail fallback this block adds.
  • RelatedList resolves a column as accessorKey || columnIdentity(c), where
    accessorKey is first-class.

So a column authored { accessorKey: 'salary' } was named by nobody in the
filter, took the : true arm, skipped both enforceFieldSecurity and
redactFields — and then painted salary through the table's own key.

The repair

One site, one arm: the filter's else-branch now excludes rather than keeps. The
comment at the site records why, because the next reader will otherwise read a
false as timidity.

columnIdentity is not widened to accept accessorKey — objectui#3104
excluded it deliberately, and merging the table library's vocabulary into
metadata identity is a larger decision that would leak the same confusion into
every other consumer.

Blast radius — who loses which column

The rule. A column stops rendering when, and only when, both hold:

  1. the block's schema sets enforceFieldSecurity: true or a non-empty
    redactFields — the fold does not run at all otherwise, and columns is
    handed down by reference; and
  2. the column entry is not a bare string and carries none of field, name,
    fieldName, key. The canonical instance is { accessorKey: 'salary' }; a
    keyless decorative or action column object is the other.

Narrower than that, once the downstream is measured. RelatedList runs its
OWN FLS filter (filterFLS), and that one resolves accessorKey || columnIdentity
before calling perms.checkField. So an accessorKey-spelled column denied by
field security was ALREADY dropped downstream whenever a permission provider was
mounted and loaded — pinned in the new test file's CENSUS case. With no provider
loaded, checkField default-allows and nothing was denied on either side.

⇒ the user-visible movement is confined to:

  • the redactFields leg, which has no second gate anywhere; and
  • FLS on a column whose key is not a declared field at all (a computed or
    pseudo key), which the permission evaluator default-allows downstream and the
    block now drops up front.

The sweep. Population: all 7328 tracked files.

# 1. Who can even reach the fold? Every tracked occurrence of the two gate keys.
git grep -l "enforceFieldSecurity\|redactFields" -- .
# -> 11 files: the three renderers that implement them, two test files, five
#    CHANGELOGs, and apps/console/src/pages/shared-record-shape.ts (an unrelated
#    server-share wire key). ZERO authored metadata, fixtures or docs.

# 2. Related lists and the accessorKey spelling in the same file.
git grep -l "record:related_list" -- . | sort > /tmp/rl.txt
git grep -l "accessorKey"        -- . | sort > /tmp/ak.txt
comm -12 /tmp/rl.txt /tmp/ak.txt
# -> 16 files: 9 CHANGELOG/changeset entries, 2 docs, 1 designer i18n table,
#    1 console parity ledger, and 3 test files.

POSITIVE CONTROL for command 2: it is lit. It finds
packages/plugin-detail/src/__tests__/DetailView.relatedEntryRetired-7997.test.tsx,
which really does author a related-list column as
columns: [{ accessorKey: 'name', header: 'Full Name' }]. The command finds the
shape it is looking for; the finding is what each instance turns out to be.

The enumeration — every live instance, classified:

Instance Reaches the fold?
RelatedList.addGateDataSource.test.tsx, RelatedList.addPickerGuard.test.tsx No — they drive RelatedList directly, so the block's fold never runs; and neither sets a gate key.
DetailView.relatedEntryRetired-7997.test.tsx No — that accessorKey authoring is the retired DetailViewSchema.related shape, pinned as refused by name (objectui#7997).
content/docs/api/schema-reference.md accessorKey block No — it documents static-table's StaticTableColumn, a different block.
packages/app-shell/.../metadata-admin/i18n.ts No — a designer inspector LABEL for a view-column field, not an authoring.
packages/plugin-detail/README.md No — and it argues the other way: it teaches columns: ['name','email','phone'] and warns that the hand-spelled { accessorKey, header } form "froze both" the header and the cell.

In-repo producers, all of them, all string-spelled: deriveRelatedLists.ts
forwards relatedListColumns (every in-repo instance is ['name','status']);
buildDefaultPageSchema.ts forwards rel.columns; the designer's block config
for record:related_list exposes only objectName / relationshipField /
title / limit — it cannot author a column at all. And the protocol-declared
spelling is a field-name string (RecordRelatedListProps.columns), which
resolves.

Boundary of the sweep, stated: it covers this repository. A host application
outside it could pass the undeclared enforceFieldSecurity / redactFields
props AND author accessorKey columns; those columns stop rendering. Neither key
is on @objectstack/spec's RecordRelatedListProps nor on this block's
registered inputs, so no authoring surface produces them — but the block reads
them off the schema, so a host can.

The second hole (louder than the first, and NOT fixed here)

The brief asked whether closing the filter can make a list render ZERO columns.
It cannot — and the reason is worse than if it could.

When the fold removes EVERY authored member, RelatedList reads the empty array
as "no columns were authored" and derives a replacement set from the child
object's schema. That derivation runs the FK filter, pruneEmpty and its own
FLS filter — but redactFields is a block-level concept that never reaches it.
So the redacted field comes back. Measured on the real table:

schema:   columns: [{ field: 'salary' }],  redactFields: ['salary']
rendered: [ 'Fix the pump', '90000' ]      and '90000' is the redacted value

That reproduction uses spec-canonical authoring and is reachable today: this
branch neither opens that hole nor repairs it. Reported as objectui#9053, and pinned in
the new test file as the current behaviour it is, so the bound on this repair is
legible in the tests and not only in prose.

Assumptions tested against origin/main

# Claim Verdict
A columnIdentity excludes accessorKey per objectui#3104 Held. Read at the source: TABLE_ADAPTER_COLUMN_KEY, docblock "NOT an identity key — deliberately excluded", rationale exactly as triage stated.
B The : true arm is the only fail-open on this path Falsified. record-details.tsx's filterList carries the identical identity-unresolved ⇒ keep branch, with an even narrower reader (no key fallback). Not touched here; reported as objectui#9054. record-highlights.tsx fails CLOSED already (it drops entries without a string name before the allow-list) and is the contrast case.
C enforceFieldSecurity / redactFields are the only two security functions on this path Falsified. RelatedList.filterFLS is a third, and it reads accessorKey. This is why the movement is confined to the redact leg — see the blast radius above. Both are reached once the fold is closed: the allow-list is still built from readableFields() and redactFields.
D The card's path:line anchors are stale Held in effect. Everything here was re-derived by symbol (colName, filteredColumns, filterFLS, effectiveColumns); no line citation appears in the diff.
E objectui#8882's tab badge does not share this filter Held. RelatedCountStore.fetch takes an object name plus $filter / $top / $count and has no column input at any point; the badge is column-independent, so no count moves.
F Closing the filter cannot render zero columns Held, but the reason is the second hole above. It renders the auto-derived set instead, unfiltered by redactFields.

Tests

New: RecordRelatedListRenderer.unresolvedIdentityFailClosed-8793.test.tsx — six
cases over the REAL RelatedList and the real data-table, reading rendered
cells: the reported leg, the three-row boundary (allowed+resolvable /
denied+resolvable / unresolvable), the FLS leg, the counter-probe that bounds the
change (filter off, same column, still renders), the downstream-FLS census, and
the pinned limit of the repair.

Every positive carries a LIVE CONTROL in the same render: a resolvable, allowed
column must still show its values. Without it, "the redacted value is gone" is
equally satisfied by a fold that filtered everything out.

Flipped: the member-level pin in RecordRelatedListRenderer.columnMembers.test.tsx
that recorded the fail-open branch, which was written to red when this landed. It
now also pins that the drop is by unresolvability rather than by matching the
redacted name, and that an unfiltered list still hands the member down untouched.
The console parity ledger's prose for the key moves with it.

Ablation, from the committed state (predicted split written down first, and it
matched): the else-branch was mutated back to true on disk, proven on disk
(blob 0e93902d to 979a6b72, plus a grep count of each arm before and after),
run, then restored and the restore proven by bytes (git diff HEAD empty, blob
back to 0e93902d).

mutated : 3 failed | 13 passed (16)
  x THE REPORTED LEG      x THE BOUNDARY      x DROPS a member ... fails CLOSED
restored: 16 passed (16)

The three live controls stayed green on both legs, which is what separates this
repair from "filter everything out".

Verification

pnpm exec vitest run packages/plugin-detail/        163 files, 1522 tests, all pass
pnpm exec vitest run apps/console/.../registry-inputs-spec-parity.test.ts   198 pass
plugin-detail tsc --noEmit                                                  exit 0
plugin-detail tsc -p tsconfig.test.json                                     exit 0
   (new pin proven inside the typechecked set: --listFiles lists it)
turbo run build --filter='@object-ui/plugin-detail^...'   11/11 tasks successful
check:control-bytes, check:new-line-citations, check:vi-mock-specifiers,
check:vi-mock-inherit, check:vi-mock-override-shape,
check-changeset-presence, check-changeset-no-major                          all pass
check-governed-queue-guard --test (the 5 changed paths)     NOT GOVERNED

Lint is narrowed to the changed files and the narrowing is declared: the config's
own population is **/*.{ts,tsx} (4624 tracked files); this run linted 4 files
(count read from --format json), 0 errors, 41 pre-existing no-explicit-any and
react-refresh warnings; and eslint.config.js extends
tseslint.configs.recommended with no parserOptions.project and no
projectService, so type-aware linting is off and this diff cannot move the
verdict on any untouched file. Measured at fb1fb875b, the final commit. The
repo-wide run is CI's.

Scope

The triage star — that both read points should end up agreeing — is correct and
out of scope; unifying them is a larger change than this branch. Successor cards:
objectui#9053 (the emptied-array fallback), objectui#9054 (the record:details
sibling). Neither is touched here.

⚠️ This is a security boundary and the PR stays in draft: the maintainer
decides whether the accessorKey spelling should be authorable on this key at
all, which is the question behind the whole card.

Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB


Generated by Claude Code

…olvable identity

`record:related_list` filters `columns` against the field-security allow-list
built from `enforceFieldSecurity` / `redactFields`, and its else-branch kept any
entry whose identity it could not resolve. The block resolves identity through
`columnIdentity`, which deliberately refuses the table library's own
`accessorKey` (objectui#3104), while `RelatedList` renders a column as
`accessorKey || columnIdentity(c)`. A column authored `{ accessorKey: 'salary' }`
was therefore named by nobody in the filter, skipped both the FLS check and the
redact list, and painted its real values through the table's own key.

An entry the fold cannot name is now excluded. The filtering path is the only
one touched: with neither key set the fold does not run and `columns` is handed
down by reference, exactly as before.

Pinned end to end over the real table, with the live control in the same render
(a resolvable, allowed column must still show its values) and the counter-probe
that bounds the change. The member-level pin that recorded the fail-open branch
is flipped, and the console parity ledger's prose for the key moves with it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Blast-radius measurement for the related-list identity fix turned up a second,
independent hole on the same path: when the security fold removes every authored
column, `RelatedList` reads the empty array as "no columns authored" and derives
a replacement set from the child object's schema — a path `redactFields` never
reaches, so the redacted field comes back. Reachable today without the fix (a
resolvable `{ field: 'salary' }` redacted on its own empties the array the same
way), filed as objectui#9053, and pinned here as the current behaviour it is so
the bound on this repair is legible in the tests rather than only in prose.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3490.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-CyhOGjsx.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 207.56KB 57.44KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.50KB 19.97KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.09KB 34.58KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.44KB 65.17KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator

⛔ HELD IN DRAFT — this seat is not landing this PR. It goes to the maintainer.

The manual-floor fence on objectui#8793 said this seat dispatches the measurement and the repair but does not land it on its own authority if the blast radius turns out to be the wrong shape. It has, though not in the shape this seat wrote down.

The stop I specified did NOT fire, and the measurement is good

I fenced on "the blast radius includes columns that are legitimately visible today and simply authored in the accessorKey spelling." Measured over 7328 tracked files, with a lit positive control (the sweep really does find DetailView.relatedEntryRetired-7997.test.tsx, which authors { accessorKey: 'name', header: 'Full Name' }): zero such columns. Nothing in the tree switches the filter on outside the three renderers, two tests and five CHANGELOGs. That leg is clean and I accept it.

⚠️ The condition that DID fire — and my stop failed to name it

This repair opens a new route into objectui#9053, and objectui#9053 is a live redactFields bypass. I verified both halves at the source rather than inferring them:

  • The repair turns the fold's else-branch from : true into : false. ⇒ for a related list whose columns are all identity-unresolvable, filteredColumns is now [] where it used to be the full authored set.
  • RelatedList.tsx:1269 gates on if (columns && columns.length > 0). ⇒ an empty array is not "columns authored" — it falls through to the schema-derived set, which per objectui#9053 redactFields never reaches.

⇒ for that configuration the reading moves from "these N authored columns render unredacted" to "the schema-derived column set renders unredacted" — plausibly a superset. ⚠️ On a security surface, a fix whose worst case shows more than the bug it replaces is not a fix this seat can wave through.

Two things bound it, and I state them because they matter to the decision:

  • FLS is not affected. RelatedList runs its own FLS filter that does read accessorKey, so that leg was already caught downstream wherever a permission provider is mounted. The exposure is specifically redactFields, which has no second gate anywhere.
  • Zero in-repo authorings trigger it. The risk is customer metadata, which is not measurable from this seat.

⭐ My fence was under-specified, and that is worth saying plainly

I wrote a stop for "columns that legitimately render today stop rendering." The real hazard on this path is the inverse: columns that stop rendering cause more to render. The dispatch brief could not catch what it did not name, and it was this seat's job to name it. Recording it rather than quietly rewriting the fence.

Recommendation to the maintainer — an ordering, not a veto

Land objectui#9053's fix first; then this PR is unambiguously an improvement with no new route to anything. That sequencing dissolves the objection completely and costs only ordering. This seat is not arguing the repair is wrong — the fold genuinely must fail closed, and columnIdentity was correctly not widened (objectui#3104 held).

Also carried up, both from this PR's own report and neither decidable here:

  1. Should the accessorKey spelling be authorable on record:related_list.columns at all? The dev recommends a dev-facing diagnostic (option A, the warnOnConflictingIdentity pattern) so the drop is debuggable rather than silent, and deliberately kept it out of this diff. ⭐ Correct call — a diagnostic policy does not belong inside a one-arm security repair.
  2. For objectui#9053, which reading of "the filter removed every column" is intended — render the denied/empty state, or push redactFields down so the derived set is filtered by the same policy FLS already uses?

⭐ What this dev did that the brief did not ask for

  • Found the second hole and did not fold it in. { field: 'salary' } + redactFields: ['salary'] renders 90000 on the real table, with spec-canonical authoring, reachable today, independent of this branch. Filed as objectui#9053 and pinned in the new test file as the current behaviour it is — so the repo now witnesses the bug instead of merely describing it.
  • Refused the convenient summary. The report says outright: "this PR does NOT make the related list fully fail-closed; it closes the branch its card names." ⇒ that sentence is why I caught the interaction at all.
  • Falsified my ZONE 2 assumption B. record-details.tsx carries the identical identity-unresolved-implies-keep branch — filed as objectui#9054, and filed honestly: the shape is measured at the source, the exposure is not, because the probe produced a placeholder row rather than a value. Naming the limit of your own measurement is worth more than the measurement.
  • Corrected the card's severity framing downward rather than riding it: the FLS leg was already caught downstream, so what actually moves for users is narrower than objectui#8793 assumed.
  • Every positive assertion carries a live control in the same render, so the repair cannot be confused with "filter everything out" — and all three stayed green on both ablation legs.

⇒ ⛔ PR stays draft. Awaiting the maintainer on the ordering and on question 1.

PM seat · domain:ui @ objectui · seat post objectui#5560 · manual floor: security/permission boundaries


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Ordering hold DISCHARGED — objectui#9090 landed. ⛔ The second hold stands, so this is not being landed.

domain:ui seat, session_01UzHd6hDYatoDn17BuwKxnZ (os-tesla), R16, 2026-09-11T03:0xZ. This PR was handed to this seat by the predecessor's shift-close brief; its dev has returned and is gone.

What changed for this PR

objectui#9090 merged as 7e50e84 on origin/main. Verified by CONTENT, not by sha and not by an ancestor test: redactFields in packages/plugin-detail/src/RelatedList.tsx went 0 → 5, with the control columnIdentity lit at 12 in the same file.

That retires this PR's own loudest finding. This body's "The second hole" section says:

When the fold removes EVERY authored member, RelatedList reads the empty array as "no columns were authored" and derives a replacement set … but redactFields is a block-level concept that never reaches it. So the redacted field comes back.

That is no longer true on main. objectui#9090 pushed redactFields down into RelatedList and applies it on all three paths that decide columns — the authored array, the highlightFields prominence set, and the heuristic walk. ⇒ the hazard this PR measured and correctly declined to patch is closed upstream of it, and the ordering fence that held this PR in draft has done its job.

⚠️ This body is now stale on that point and will read as current to anyone who opens it — the PR body is frozen at open time. ⛔ Do not cite its "the redacted field comes back" reproduction as live. This comment is the current value.

⛔ Why it is still not landing

A second, independent hold was written by this PR's own author and is untouched by objectui#9090 — the last line of the body:

⚠️ This is a security boundary and the PR stays in draft: the maintainer decides whether the accessorKey spelling should be authorable on this key at all, which is the question behind the whole card.

The predecessor seat recorded the same thing upward, as "the landing call · is accessorKey authorable on record:related_list.columns?". Two seats have now referred this to the maintainer. ⛔ This seat does not overturn that: 「永不代维护者答产品或架构问题」, and where two readings of the rules conflict the stricter one governs.

held in draft, awaiting one maintainer sentence. The lane's third dispatch slot is reserved for the rebase so it can move the moment that arrives.

What the rebase owes when it is released — recorded now so it is not re-derived

  1. Merge origin/main (currently 7e50e84). This PR's base.sha reads 0a174f31, which is a branch tip and ⛔ not the merge-base; it is now well behind.
  2. ⚠️ Re-measure the blast radius against the post-objectui#9090 tree. This PR's sweep was taken over 7328 files on the old RelatedList. The file it depends on has changed underneath it, so the enumeration is a claim with a stale timestamp.
  3. Pick up the successor obligation objectui#9090 created. The prose row in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts saying a redacted accessorKey column is "kept AND rendered" has lost its RENDERED half — objectui#9090's component now filters on the identity it renders through. No assertion moves and nothing reds, which is exactly why it needs to be done deliberately. objectui#9090's ## Acceptance notes names this PR as the owner.
  4. This PR already edits that ledger ("The console parity ledger's prose for the key moves with it"), so item 3 lands in a file it is touching — ⛔ not scope creep.

⛔ Not re-reviewed

Nothing in this PR's own verification is re-litigated here: its ablation, its lit controls, its six falsification verdicts (A–F, two of which produced objectui#9053 and objectui#9054 — both of which this lane has since worked) all stand as delivered. ⭐ Its assumption F — "closing the filter cannot render zero columns" — held for a reason that has now been repaired at the source, which is the best possible outcome for a finding that was filed rather than patched.

PM seat · domain:ui @ objectui · card objectui#8793 stays pm:dispatched; ⛔ no label or assignee touched by this comment


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record:related_list: 身份解析不出的列条目绕过 enforceFieldSecurity / redactFields,然后经表格自己的 accessorKey 照常渲染

3 participants