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 }}
Repository navigation
plugin-grid: ObjectGrid re-applies field-level security only on the object-schema column path, so a host-fed grid with an authored fields projection skips it #6723
Observed while implementing objectui#6677 (PR #6722), which reordered ObjectGrid.generateColumns()'s default paths. Filing, not claiming — and NOT
fixed in that PR, which was deliberately scoped to the row-key fallback.
The asymmetry
generateColumns() re-applies field-level security to its columns at exactly
one place, in the object-schema path — packages/plugin-grid/src/ObjectGrid.tsx
(measured on efbd027cb):
if (perms?.isLoaded && schema.objectName
&& !perms.checkField(schema.objectName, fieldName, 'read')) return;
The inline-data path has no equivalent check. Both paths can serve an
object-bound grid, and which one runs depends on who fetched the rows and whether the author declared a projection:
So the same object, same declared projection, renders through a code path that
does or does not re-check FLS purely according to who owns the fetch. Before PR #6722 the third row was in the same position; that PR closed it for the
undeclared case and this is the remainder.
Why it is not obviously exploitable, and why that is not the same as fine
ListView — the dominant host — filters its own effectiveFields through FLS
before forwarding, and its source says so explicitly (ObjectGrid re-applies
FLS only on the DERIVED path, not on the explicit-columns one — so widening
here would be a widening past the field gate). So through ListView the gate is
applied host-side and the grid's omission is redundant.
The exposure, if any, is a host that is not ListView: a direct ObjectGrid schema={{ objectName, fields }} data={rows} composition, or a future
host that forwards an authored projection without filtering it. The grid would
render a column for a field the principal cannot read, populated from whatever
the host's payload contained.
Not measured in a browser — this is a code reading, and the severity call is
deliberately left to triage rather than asserted here.
The decision behind it
Closing this is not a one-line move, which is why PR #6722 left it alone: it
asks whether the grid's FLS re-check belongs on the AUTHORED projection at all,
or whether authored columns are the host's responsibility. The two paths also
differ in two other ways that would have to be settled together — the schema
path resolves labels through resolveFieldLabel (i18n) and drops names the
object does not declare (if (!field) return;), while the inline path humanises
labels locally and keeps host-joined / derived keys. Converging them without
deciding those would silently drop derived columns.
Related: objectui#6677 (the branch-order half, fixed), objectui#6598.
On origin/main at d06059f24 (main moved past the dispatch's earlier base — re-derive on your own base and say what it is):
packages/plugin-grid/src/ObjectGrid.tsx — generateColumns opens at line 1827.
The soleperms.checkField call in that file is at line 2400, in the object-schema path. git grep -n checkField origin/main -- packages/plugin-grid/src/ObjectGrid.tsx returns exactly two lines: that call and a comment above it.
That is a code reading on one ref, not a browser measurement. Everything else about path selection below is what the card says — verify it before relying on it.
Scope
Do: on the inline-data path, when perms.isLoaded && schema.objectName, filter each column whose key the object declares through perms.checkField(objectName, fieldName, 'read').
Do not: touch keys the object does not declare — host-joined and derived columns pass through exactly as today. This is the ruling's load-bearing limit, not an optimisation: the card's own analysis warns that converging the paths without it silently drops derived columns.
Explicitly out of scope (the ruling refuses them by name): merging the two paths' label handling (resolveFieldLabel i18n vs local humanize), the undeclared-key drop, and anything on the ListView host side.
Acceptance pins, from the ruling
A declared field the principal can read renders its column.
A declared field the principal cannot read does not render — even when the host supplied data for it.
A non-object (derived / host-joined) key is unaffected.
Behaviour through ListView is byte-for-byte unchanged — it pre-filters, so this fix is a no-op on that path. Pin that it is a no-op rather than assuming it.
Reporting
Ablate the guard and report which assertions move and which do not, both directions, the way this lane's recent PRs have. If pin 2 does not move when you remove the check, say so — a guard that changes nothing observable is a finding, not a fix. Report the premise as false if the inline path turns out already to filter.
{
"issue": 6723,
"status": "done",
"branch": "claude/issue-6723-inline-fls",
"pr": "https://github.com/objectstack-ai/objectui/pull/6800",
"premise_still_valid": true,
"summary": "Implemented the narrow defence-in-depth fix from the 2026-08-29 ruling: ObjectGrid.generateColumns()'s inline-data path now filters each column through perms.checkField(objectName, fieldName, 'read') when perms.isLoaded && schema.objectName, the same gate and same deferral condition the object-schema path already used. Only keys the object DECLARES are judged (read via hasOwnProperty, so an inherited name like `constructor` is not mistaken for a declared field); host-joined and derived keys pass through untouched, which is the ruling's load-bearing limit. Nothing else converged: label handling, the undeclared-key drop and the ListView host side are untouched. Base re-derived on my own worktree: d06059f24, the same ref the dispatch named; premise confirmed there (generateColumns opens at 1827, the sole perms.checkField was at 2400 in the object-schema path, the inline path had none). Note on the dispatch's assignee clause: the card was already assigned to os-sales by PM, and I did not touch it.",
"tests": "Base d06059f24, branch head 7159718f0 (the union below was re-run after the final commit, on that head). (1) New pins: packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx (8 cases: PIN 1 readable declared field renders; PIN 2 unreadable declared field does not render even with host data for it; PIN 3 derived/host-joined key unaffected; PIN 3b inherited name not mistaken for declared; CONTROLs for perms-not-loaded, no objectName, schema-in-flight row-key fallback; WIRING through the REAL PermissionProvider) and packages/plugin-list/src/__tests__/ListView.inlineFlsNoop-6723.test.tsx (3 cases, REAL ListView over REAL ObjectGrid via heavyDomTests). (2) ABLATION, both directions. Method: this branch's only edit to ObjectGrid.tsx is the guard, so the mutation is `git checkout d06059f24 -- ObjectGrid.tsx`; NO REBUILD IS INVOLVED and that is a property of this repo, not an omission - vitest.config.mts aliases every @object-ui/* specifier to that package's src and the pin imports ../ObjectGrid relatively, so the run reads the mutated source directly. Mutation CONFIRMED ON DISK before reading anything: git hash-object of the file == base blob 0988ddca0 and != head blob 2b55805e4, with marker count objectui#6723 2->0 and perms.checkField 2->1. Restore ran from a `trap ... EXIT INT TERM` with an absolute path and was proved by an empty `git diff HEAD` plus disk hash == HEAD blob 2b55805e4 (not by an exit code). ABLATED RESULT: `Tests 2 failed | 9 passed (11)`. The two that MOVED are PIN 2 and WIRING - the same assertion asked once of the stub and once of the real PermissionProvider - both `AssertionError: expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]`. The ones that DID NOT MOVE: PIN 1, PIN 3, PIN 3b, the three CONTROLs, and all three ListView cases. So pin 2 does move, and it moves through the real provider - the guard is not a no-op overall. PIN 4 (ListView) not moving IS the pin: it is the measured no-op the ruling asserted, and its mechanism is pinned too (authored columns return from the normalizeColumns branch before the inline path; unauthored routes to the object-schema path, pinned by the ABSENCE of the hidden `Id` column, which only the row-key derivation would keep). RESTORED: `Test Files 2 passed (2) / Tests 11 passed (11)`. (3) Affected-package suites: `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/` -> `Test Files 151 passed (151) / Tests 1613 passed (1613)`. (4) `pnpm exec vitest run scripts/` -> `Test Files 88 passed (88) / Tests 2447 passed (2447)`, owed because the diff edits vitest.config.mts, which several scripts/__tests__ helpers read. (5) type-check: `pnpm --filter @object-ui/plugin-grid type-check` and the same for plugin-list, both clean; each is `tsc --noEmit && tsc -p tsconfig.test.json`, and `--listFiles` returns 1 hit for each new test file, so the tests really are in the program. (6) Gates, each quoted from its own printed verdict line rather than from a piped exit code: check-control-bytes OK (5647 files); check-vi-mock-specifiers OK (491 carry a mock) - owed because the new grid pin adds a vi.mock('@object-ui/permissions'); check-changeset-presence OK (3 source files of 2 released packages, 1 changeset); check-changeset-fixed OK; check-changeset-no-major OK; check-phantom-dependencies OK; check-side-effects-array OK. (7) Lint, DECLARED NARROWING: `pnpm --filter @object-ui/plugin-grid lint`, the same for plugin-list, and `pnpm lint:root` -> 0 errors in all three (683/442/28 pre-existing warnings, eslint exits 0). Population read from eslint's own config via `--format json`: 134 and 65 files judged. eslint.config.js sets no `project`/`projectService`, so type-aware linting is off and this diff cannot move the verdict on any file it does not contain. Heavy runs went through the shared lock (scripts/pm/os-verify-lock.sh), all VERDICT command-exit 0. CI not waited on, per the standing contract.",
"mcp_calls": "0 - the MCP/GraphQL pool was rate-limited this session as the dispatch noted, so every GitHub read and write (issue thread, dedupe listing, issue creation, PR creation, both read-backs) went through repo-scoped REST via the proxy. Channel switch declared. Dedupe control validated: the listing that found no duplicate DID return #6723 itself.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6799: after this PR, the authored `columns` path (normalizeColumns(schemaColumns), both the ListColumn[] and string[] arms) is the one remaining generateColumns() path with no FLS re-check - same defect class, and reachable without the host supplying rows at all, since that path runs whether the grid fetches or not. The ruling scoped this card to the inline-data path, so it is not touched here. The issue records why it is a decision rather than a mechanical follow-up: the declared-key limit has to be read through columnIdentity/resolvesToDataColumn for ListColumn[] rather than off a bare string, and objectui#6598's hasAuthoredColumns rationale is written around the current answer and would need rewriting in the same change. Filed unassigned and unlabelled for PM triage."
]
}
Verification. Check-runs table on head 7159718f0: total_count=30, 30 rows, 27 success + 3 skipped, zero non-terminal.
The ruling's load-bearing limit was implemented as a limit, not as an approximation
The ruling's whole point was that only object-declared keys get judged, so host-joined and derived columns keep passing. That was implemented — and sharpened past what the order specified: declared-key membership is read through hasOwnProperty, so an inherited name like constructor is not mistaken for a declared field. The order did not ask for that; it is the difference between "the key is in the object" and "the object declares the key", and getting it wrong would have silently dropped a column named after a prototype member.
Nothing else converged. Label handling, the undeclared-key drop and the ListView host side are untouched, exactly as the ruling required.
The ablation reads correctly in both directions
Moved (2 of 11): PIN 2 and WIRING — which are the same assertion asked once of a stub and once of the real PermissionProvider, both failing as expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]. That the real-provider leg moves too is what makes this more than a stub test agreeing with itself.
⭐ PIN 4 not moving IS the pin. It is the no-op through ListView that the ruling asserted — and rather than assuming it, the PR pinned the mechanism: authored columns return from the normalizeColumns branch before the inline path is reached, and the unauthored case routes to the object-schema path, pinned by the absence of the hidden Id column that only row-key derivation would have kept. Pinning why a no-op is a no-op is the part most reports skip.
The remaining non-movers (PIN 1, PIN 3, PIN 3b, three CONTROLs) are the deferral and pass-through cases, which is the right shape: the guard is measured to discriminate, not merely to fire.
Review notes
The mutation was proven on disk by blob hash against the base and head blobs plus marker counts (checkField 2 → 1), never by an editor exit code, and the restore proven by an empty git diff HEAD plus a hash match — not by a command's return value.
Running pnpm exec vitest run scripts/ was owed and correctly identified: the diff edits vitest.config.mts, which several scripts/__tests__ helpers read. That is the fix(react,core): declare the spec dataSource binding at the gate-wrapping seam #6767 lesson applied without being told — a change to shared test infrastructure is not covered by the affected packages' own suites.
Lint narrowing declared with all three pieces of evidence (population read from eslint's own --format json, no project/projectService so type-aware linting is off, therefore the diff structurally cannot move an untouched file's verdict).
The out-of-scope finding is the important output
#6799 — after this lands, the authored columns path is the last of generateColumns()'s three paths with no FLS re-check, and it is more reachable than the one just fixed: it runs whether or not the host supplies rows.
This seat has routed it to the decision box with a facets block rather than leaving it bare, because the sibling question was ruled today on reasoning that transfers almost word for word, and a third unruled path sitting beside two settled ones is the state most likely to be misread later as already handled. ⚠️ It also surfaces something that is not a mere follow-up: ListView.tsx's hasAuthoredColumns docblock states the current behaviour as settled fact, and #6598 deliberately depends on it — when the field gate strips every authored column, ListView keeps sending the empty projection precisely because the grid will not re-check. So closing that path means rewriting a rationale, not just adding a predicate. Filing it unassigned and unlabelled for PM triage was the right call.
Activity
huangyiirene commentedon Aug 29, 2026
分诊:入决策箱 + 四棱块
needs-user-decision·domain:ui·security·priority:p1· typeBug。字段级安全属安全/权限边界 ⇒ 人工地板,⛔ 不代裁。<!-- os-decision-facets -->
一句话问题:
ObjectGrid只在对象 schema 路径重新施加字段级安全;host 传data且作者声明了fields时走内联路径,不重查。同一对象、同一投影,查不查 FLS 取决于谁取的数。选项 × 真实客户可感成本
resolveFieldLabel(i18n)且丢弃对象未声明的名字;内联路径本地美化标签并保留 host 拼接/派生列。⇒ 不设计好就会静默丢掉派生列四棱⚠️ 今天大概率为零 —— 主力 host
① 长远合理性:B 是「单一责任点」,A 是「纵深防御」。安全面上本仓一贯取向是后者,但 A 在这里不是免费的(见上表)。
② 业务拉动:
ListView自己先按 FLS 过滤effectiveFields再转发,其源码明说了。⇒ 经ListView这条路,grid 的遗漏是冗余而非漏洞。③ 防 AI 犯错:⭐ 分水岭在这里。B 若只是「不做」而不写下来,下一个直接组合
ObjectGrid schema={{objectName, fields}} data={rows}的作者(人或 AI)没有任何东西告诉他要自己过滤。⇒ B 必须带文档,否则 B 比 A 危险。④ 不扩散:B 零新增;A 要同时处理标签与派生列两处差异,是一次真正的收敛工作。
推荐:⛔ 不代裁。只指出一点:「B 不带文档」不是选项——那是把一个隐式责任留给未来的作者,而这正是本卡描述的缺陷得以存在的原因。
⭐ 置信缺口
没有人清点过非
ListView的 host。 卡面把暴露面写成假设句(「a host that is not ListView」「a future host」),没有实际枚举本仓与 hotcrm 里直接组合ObjectGrid并传data+fields的站点。这个数字决定一切:⇒ 建议裁前先跑这次清点。⛔ 本席未跑 —— 它落在执行面。
低摩擦裁决:回「先清点 host」(推荐)/「A」/「B + 文档」。
裁后执行段:清点为 0 ⇒ 多半走 B + 文档,转⚠️ dispatch 必须同时申报标签解析与派生列两处差异,⛔ 不得静默收敛。裁 B ⇒ 转
pm:queue。裁 A ⇒ 转pm:queue,pm:queue,落点是ObjectGrid的契约文档 + host 侧说明。Generated by Claude Code
claude commentedon Aug 29, 2026
Claim + dispatch order —
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round R15.Branch:
claude/issue-6723-inline-fls. Oneos-devagent, one worktree.The ruling this implements (verbatim, 照抄不译)
Terrain I verified myself
On
origin/mainatd06059f24(main moved past the dispatch's earlier base — re-derive on your own base and say what it is):packages/plugin-grid/src/ObjectGrid.tsx—generateColumnsopens at line 1827.perms.checkFieldcall in that file is at line 2400, in the object-schema path.git grep -n checkField origin/main -- packages/plugin-grid/src/ObjectGrid.tsxreturns exactly two lines: that call and a comment above it.That is a code reading on one ref, not a browser measurement. Everything else about path selection below is what the card says — verify it before relying on it.
Scope
Do: on the inline-data path, when
perms.isLoaded && schema.objectName, filter each column whose key the object declares throughperms.checkField(objectName, fieldName, 'read').Do not: touch keys the object does not declare — host-joined and derived columns pass through exactly as today. This is the ruling's load-bearing limit, not an optimisation: the card's own analysis warns that converging the paths without it silently drops derived columns.
Explicitly out of scope (the ruling refuses them by name): merging the two paths' label handling (
resolveFieldLabeli18n vs local humanize), the undeclared-key drop, and anything on theListViewhost side.Acceptance pins, from the ruling
ListViewis byte-for-byte unchanged — it pre-filters, so this fix is a no-op on that path. Pin that it is a no-op rather than assuming it.Reporting
Ablate the guard and report which assertions move and which do not, both directions, the way this lane's recent PRs have. If pin 2 does not move when you remove the check, say so — a guard that changes nothing observable is a finding, not a fix. Report the premise as false if the inline path turns out already to filter.
Generated by Claude Code
fix(plugin-grid): re-apply field-level security on the inline-data co…
test(plugin-grid): record the measured ablation in the #6723 pin's he…
claude commentedon Aug 29, 2026
os-dev-report
{ "issue": 6723, "status": "done", "branch": "claude/issue-6723-inline-fls", "pr": "https://github.com/objectstack-ai/objectui/pull/6800", "premise_still_valid": true, "summary": "Implemented the narrow defence-in-depth fix from the 2026-08-29 ruling: ObjectGrid.generateColumns()'s inline-data path now filters each column through perms.checkField(objectName, fieldName, 'read') when perms.isLoaded && schema.objectName, the same gate and same deferral condition the object-schema path already used. Only keys the object DECLARES are judged (read via hasOwnProperty, so an inherited name like `constructor` is not mistaken for a declared field); host-joined and derived keys pass through untouched, which is the ruling's load-bearing limit. Nothing else converged: label handling, the undeclared-key drop and the ListView host side are untouched. Base re-derived on my own worktree: d06059f24, the same ref the dispatch named; premise confirmed there (generateColumns opens at 1827, the sole perms.checkField was at 2400 in the object-schema path, the inline path had none). Note on the dispatch's assignee clause: the card was already assigned to os-sales by PM, and I did not touch it.", "tests": "Base d06059f24, branch head 7159718f0 (the union below was re-run after the final commit, on that head). (1) New pins: packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx (8 cases: PIN 1 readable declared field renders; PIN 2 unreadable declared field does not render even with host data for it; PIN 3 derived/host-joined key unaffected; PIN 3b inherited name not mistaken for declared; CONTROLs for perms-not-loaded, no objectName, schema-in-flight row-key fallback; WIRING through the REAL PermissionProvider) and packages/plugin-list/src/__tests__/ListView.inlineFlsNoop-6723.test.tsx (3 cases, REAL ListView over REAL ObjectGrid via heavyDomTests). (2) ABLATION, both directions. Method: this branch's only edit to ObjectGrid.tsx is the guard, so the mutation is `git checkout d06059f24 -- ObjectGrid.tsx`; NO REBUILD IS INVOLVED and that is a property of this repo, not an omission - vitest.config.mts aliases every @object-ui/* specifier to that package's src and the pin imports ../ObjectGrid relatively, so the run reads the mutated source directly. Mutation CONFIRMED ON DISK before reading anything: git hash-object of the file == base blob 0988ddca0 and != head blob 2b55805e4, with marker count objectui#6723 2->0 and perms.checkField 2->1. Restore ran from a `trap ... EXIT INT TERM` with an absolute path and was proved by an empty `git diff HEAD` plus disk hash == HEAD blob 2b55805e4 (not by an exit code). ABLATED RESULT: `Tests 2 failed | 9 passed (11)`. The two that MOVED are PIN 2 and WIRING - the same assertion asked once of the stub and once of the real PermissionProvider - both `AssertionError: expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]`. The ones that DID NOT MOVE: PIN 1, PIN 3, PIN 3b, the three CONTROLs, and all three ListView cases. So pin 2 does move, and it moves through the real provider - the guard is not a no-op overall. PIN 4 (ListView) not moving IS the pin: it is the measured no-op the ruling asserted, and its mechanism is pinned too (authored columns return from the normalizeColumns branch before the inline path; unauthored routes to the object-schema path, pinned by the ABSENCE of the hidden `Id` column, which only the row-key derivation would keep). RESTORED: `Test Files 2 passed (2) / Tests 11 passed (11)`. (3) Affected-package suites: `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/` -> `Test Files 151 passed (151) / Tests 1613 passed (1613)`. (4) `pnpm exec vitest run scripts/` -> `Test Files 88 passed (88) / Tests 2447 passed (2447)`, owed because the diff edits vitest.config.mts, which several scripts/__tests__ helpers read. (5) type-check: `pnpm --filter @object-ui/plugin-grid type-check` and the same for plugin-list, both clean; each is `tsc --noEmit && tsc -p tsconfig.test.json`, and `--listFiles` returns 1 hit for each new test file, so the tests really are in the program. (6) Gates, each quoted from its own printed verdict line rather than from a piped exit code: check-control-bytes OK (5647 files); check-vi-mock-specifiers OK (491 carry a mock) - owed because the new grid pin adds a vi.mock('@object-ui/permissions'); check-changeset-presence OK (3 source files of 2 released packages, 1 changeset); check-changeset-fixed OK; check-changeset-no-major OK; check-phantom-dependencies OK; check-side-effects-array OK. (7) Lint, DECLARED NARROWING: `pnpm --filter @object-ui/plugin-grid lint`, the same for plugin-list, and `pnpm lint:root` -> 0 errors in all three (683/442/28 pre-existing warnings, eslint exits 0). Population read from eslint's own config via `--format json`: 134 and 65 files judged. eslint.config.js sets no `project`/`projectService`, so type-aware linting is off and this diff cannot move the verdict on any file it does not contain. Heavy runs went through the shared lock (scripts/pm/os-verify-lock.sh), all VERDICT command-exit 0. CI not waited on, per the standing contract.", "mcp_calls": "0 - the MCP/GraphQL pool was rate-limited this session as the dispatch noted, so every GitHub read and write (issue thread, dedupe listing, issue creation, PR creation, both read-backs) went through repo-scoped REST via the proxy. Channel switch declared. Dedupe control validated: the listing that found no duplicate DID return #6723 itself.", "open_questions": [], "out_of_scope_findings": [ "filed as #6799: after this PR, the authored `columns` path (normalizeColumns(schemaColumns), both the ListColumn[] and string[] arms) is the one remaining generateColumns() path with no FLS re-check - same defect class, and reachable without the host supplying rows at all, since that path runs whether the grid fetches or not. The ruling scoped this card to the inline-data path, so it is not touched here. The issue records why it is a decision rather than a mechanical follow-up: the declared-key limit has to be read through columnIdentity/resolvesToDataColumn for ListColumn[] rather than off a bare string, and objectui#6598's hasAuthoredColumns rationale is written around the current answer and would need rewriting in the same change. Filed unassigned and unlabelled for PM triage." ] }Generated by Claude Code
claude commentedon Aug 29, 2026
ACCEPT — PR #6800
Verification. Check-runs table on head
7159718f0:total_count=30, 30 rows, 27success+ 3skipped, zero non-terminal.The ruling's load-bearing limit was implemented as a limit, not as an approximation
The ruling's whole point was that only object-declared keys get judged, so host-joined and derived columns keep passing. That was implemented — and sharpened past what the order specified: declared-key membership is read through
hasOwnProperty, so an inherited name likeconstructoris not mistaken for a declared field. The order did not ask for that; it is the difference between "the key is in the object" and "the object declares the key", and getting it wrong would have silently dropped a column named after a prototype member.Nothing else converged. Label handling, the undeclared-key drop and the
ListViewhost side are untouched, exactly as the ruling required.The ablation reads correctly in both directions
Moved (2 of 11): PIN 2 and WIRING — which are the same assertion asked once of a stub and once of the real
PermissionProvider, both failing asexpected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]. That the real-provider leg moves too is what makes this more than a stub test agreeing with itself.⭐ PIN 4 not moving IS the pin. It is the no-op through
ListViewthat the ruling asserted — and rather than assuming it, the PR pinned the mechanism: authored columns return from thenormalizeColumnsbranch before the inline path is reached, and the unauthored case routes to the object-schema path, pinned by the absence of the hiddenIdcolumn that only row-key derivation would have kept. Pinning why a no-op is a no-op is the part most reports skip.The remaining non-movers (PIN 1, PIN 3, PIN 3b, three CONTROLs) are the deferral and pass-through cases, which is the right shape: the guard is measured to discriminate, not merely to fire.
Review notes
checkField2 → 1), never by an editor exit code, and the restore proven by an emptygit diff HEADplus a hash match — not by a command's return value.pnpm exec vitest run scripts/was owed and correctly identified: the diff editsvitest.config.mts, which severalscripts/__tests__helpers read. That is the fix(react,core): declare the specdataSourcebinding at the gate-wrapping seam #6767 lesson applied without being told — a change to shared test infrastructure is not covered by the affected packages' own suites.--format json, noproject/projectServiceso type-aware linting is off, therefore the diff structurally cannot move an untouched file's verdict).The out-of-scope finding is the important output
#6799 — after this lands, the authored
columnspath is the last ofgenerateColumns()'s three paths with no FLS re-check, and it is more reachable than the one just fixed: it runs whether or not the host supplies rows.This seat has routed it to the decision box with a facets block rather than leaving it bare, because the sibling question was ruled today on reasoning that transfers almost word for word, and a third unruled path sitting beside two settled ones is the state most likely to be misread later as already handled.⚠️ It also surfaces something that is not a mere follow-up:
ListView.tsx'shasAuthoredColumnsdocblock states the current behaviour as settled fact, and #6598 deliberately depends on it — when the field gate strips every authored column,ListViewkeeps sending the empty projection precisely because the grid will not re-check. So closing that path means rewriting a rationale, not just adding a predicate. Filing it unassigned and unlabelled for PM triage was the right call.Landing once the merge queue ahead of it drains.
Generated by Claude Code
fix(plugin-grid): re-apply field-level security on ObjectGrid's inlin…