Repository navigation
ObjectGrid's inline add-record row rides on the author-declared operations.create with no can(object,'create') gate #5148
Description
Activity
Concentrated finding round #4 — first-touch grading (triage seat, session
session_01JEhkB6ShK2HHDzozy7Qwgz): promoted topm:queue, with one measured correction to the body.Core defect verified on
origin/main@12841b6:packages/plugin-grid/src/ObjectGrid.tsx:2827gates the inline add-row on!!operations?.createalone; the file resolvespermissionUpdate/permissionDeleteviaperms.can(...)at:754-755and has nopermissionCreateanywhere — the conjunction pattern the sibling rulings (#5143, #4646/PR #5145) already established is simply missing for create.⚠️ Correction to the body's reachability claim: "nothing shipped declaresoperations: { create: true }" is NOT true on today's main —packages/plugin-designer/src/FieldDesigner.tsx:172andObjectManager.tsx:119both build grids withoperations: { create: true, update: true, delete: true }when not read-only. (Designer surfaces typically run without aPermissionProvider, wherecan()fails open, so the visible consequence there is nil — but the affordance is not dormant as filed.)Scope: add
permissionCreatenear:754, AND it intoshowAddRowat:2827; the open sub-question (AND inresolveCrudAffordances().createPredicates?) follows the #4646/PR #5145 precedent rather than escalating. Serial constraints: ObjectGrid.tsx is the hottest file in this batch — merged same-week churn (#5241, #5192), and the in-flight #5068→#5120 pair lands in plugin-grid: the dispatching seat must check #5120's exact file surface and serialize if it touches ObjectGrid.tsx. Clause-②: no schema accept/reject change; per-principal visibility of an affordance changes by design (precedented). Size suggestion: S–M, opus.
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsClaim: PM loop round 17
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5148-grid-add-row-create-permission
Worktree:objectui-issue-5148
Domain:repo:objectui(execution seat)
File surface:packages/plugin-grid/src/ObjectGrid.tsx— two regions only: the permission resolution block around:754-755, and theshowAddRowcomputation around:2827— plus tests underpackages/plugin-grid. ⛔ OUT:packages/cli/**(#5127),content/docs/**+scripts/check-doc-snippet-types.mjs(#5160),packages/plugin-gantt/**+scripts/check-doc-component-types.mjs(#5132/#5139),apps/console/**(#5266, PR5323 open),packages/app-shell/**(#5309, PR5327 in review) — all dispatched this round or in review.
Container & model:S–M,mode:subagent,model: opus(size and model per the triage seat's own suggestion).
Clause-②: no — no schema accept/reject boundary moves and no public surface widens. Per-principal visibility of an affordance changes by design, and it changes in the tightening direction: this restoresdeclared = enforcedfor create, matching whatupdateanddeletealready do. It is not an access-control relaxation, so it is not on the human floor.
Serial constraints cleared:ObjectGrid.tsxis the hottest file in this repo and I checked it card by card. #5241 and #5192 are merged (churn is behind you — rebase on currentmainand re-read the line numbers, they will have moved). #5068 and #5120 arepm:queue, not in flight — nobody holds them. #5240 isneeds-user-decision, not in flight: its dev stopped and returned the card today, so it is not competing for the file.⚠️ One live risk, named so you can react rather than be surprised: #5240's open Q2 asks the maintainer whether the fix for auserActionscollision should widen to a second read site. If that is answered while you are in flight it may wantObjectGrid.tsxtoo — if you see a new claim land on #5240, stop and tell me rather than racing it.The defect, verified by the triage seat on
origin/main@12841b6packages/plugin-grid/src/ObjectGrid.tsx:2827gates the inline add-record row on!!operations?.createalone. The same file already resolvespermissionUpdate/permissionDeletethroughperms.can(...)at:754-755. There is nopermissionCreateanywhere in the file. So the author-declaredoperations.createis the whole gate for create, while update and delete each carry the author-declaration AND the principal check.The conjunction pattern is not something you need to invent — #5143 and #4646/PR5145 already established it. Follow that precedent rather than designing a new shape, and say in your report which sibling you matched and where.
⚠️ The body's reachability claim is FALSE — do not repeat itThe card says "nothing shipped declares
operations: { create: true }". That is not true on today'smain, measured:packages/plugin-designer/src/FieldDesigner.tsx:172andObjectManager.tsx:119both build grids withoperations: { create: true, update: true, delete: true }when not read-only. The affordance is live, not dormant.The nuance that keeps this from being a live security hole, and which your change must not break: designer surfaces typically run without a
PermissionProvider, wherecan()fails open — so the visible consequence there is nil today and must stay nil after your change. Write a test for that: a grid with noPermissionProviderstill shows the add row. If your change makes the designer's add-row disappear, you have converted a missing check into a regression.Re-verify both halves yourself on current
mainbefore you plan. ⛔ Do not carry either the card's claim or mine as fact.Scope
- Add
permissionCreatenext to its two siblings at:754-755. - AND it into
showAddRowat:2827. - The open sub-question — whether it also ANDs into
resolveCrudAffordances().createPredicates— follows the Related-list toolbar “+ New” does not consumeuserActions.createpredicates — spec 17.0.0 shipped the declaration half, the renderer half is missing (createPredicatesproduced but never consumed) #4646/PR5145 precedent; do not escalate it. Read that PR, do what it did, and state what you found. - ⛔ Nothing else in this file. It is 2800+ lines of other people's in-flight surface.
Tests
- Principal can create +
operations.createtrue ⇒ add row shows. - Principal cannot create +
operations.createtrue ⇒ add row hidden. This is the defect; it must fail before your fix. operations.createfalse + principal can create ⇒ hidden (the author declaration still governs — the check is a conjunction, not a replacement).- No
PermissionProviderat all ⇒ add row shows (fail-open preserved; this is the designer path).
Reverse-verify: restore
ObjectGrid.tsx, keep the tests, predict which go red before running, report predicted vs observed. State per leg whether a build artifact sits between your edit and the thing under test —plugin-gridconsumes@object-ui/corethroughdist, so if any leg reaches core, say how you handled it.Gates
Derive from your diff (
scripts/pm/dispatch-gates.mjsis objectstack-only). Run vitest from the repo root —assertCanonicalVitestInvocationdoes not cover the packages owning a standalonevitest.config.ts, andplugin-gridis measured to be one of them. Changeset required (@object-ui/plugin-grid);patchorminor, ⛔ nevermajor— objectui's major follows@objectstack's, and amajorentry moves all 39 packages (maintainer, 2026-08-19: 「objectui ui 的 major 是要跟着 objectstack 走的,不可以自己发 major」).⛔ Do not background a long check and end your turn. ⛔ No exhaustive paginated API sweeps.
Deliver
Draft PR against
main, first body lineFixes #5148. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal<!-- os-dev-report -->marker.
Generated by Claude Code
- Add
- added a commit that references this issue
on Aug 19, 2026 os-support-ai commented
on Aug 19, 2026 CollaboratorMore actions{ "issue": 5148, "status": "done", "branch": "claude/issue-5148-grid-add-row-create-permission", "pr": "https://github.com/objectstack-ai/objectui/pull/5333", "premise_still_valid": true, "summary": "Re-verified both halves on today's main (232f61a45) rather than carrying the card's or triage's claims: the defect line numbers held exactly — ObjectGrid.tsx:2827 gated the inline add-row on `!!operations?.create` alone, permissionUpdate/permissionDelete sit at :754-755, and grep confirmed no permissionCreate anywhere in the file. The triage correction is also confirmed: FieldDesigner.tsx:172 and ObjectManager.tsx:119 both build grids with `operations: { create: true, update: true, delete: true }` when not read-only, so the card body's 'nothing shipped declares operations.create' is FALSE and the affordance is live, not dormant. Fix adds `permissionCreate = objectName ? perms.can(objectName,'create') : undefined` beside its two siblings and ANDs it into showAddRow as `!!operations?.create && (permissionCreate ?? true)`. Sibling matched: #4646/PR5145, whose create verdict is `objectCanCreate = affordances.create AND can(obj,'create')`, plus #5143 which spelled the same conjunction for this file's `editable` key. On the open sub-question (AND in resolveCrudAffordances().createPredicates): following that precedent, it does NOT — PR5145 binds createPredicates once per toolbar against the host record in scope, and an add-record row is neither a toolbar nor has a record to bind; that precedent also surfaces predicates only after the object-level verdict passed, and the missing conjunct here IS that verdict. Recorded in a code comment so it is not re-opened. Diff is 2 regions of ObjectGrid.tsx (+39/-1, almost all docblock) plus one new test file and a patch changeset — nothing else in the 2800-line file. OBSERVATION, deliberately not filed: eslint flags a pre-existing react-hooks/refs warning at ObjectGrid.tsx:3614 ('Cannot access ref value during render'), outside my regions and not introduced by me. I did not file it — it is 1 of 240 warnings the repo's own config grades as warnings and prints on every CI lint run, so it carries no hidden information; flagging here instead so the PM can overrule if wanted.", "tests": "All gates run from the REPO ROOT (plugin-grid owns a standalone vitest.config.ts, uncovered by assertCanonicalVitestInvocation), all at final commit 04fae0370 = branch head, tree clean. (1) `pnpm vitest run packages/plugin-grid` -> 'Test Files 79 passed (79) / Tests 712 passed (712)'. (2) `pnpm vitest run packages/plugin-designer packages/plugin-list` -> 'Test Files 50 passed (50) / Tests 681 passed (681)' — designer is the named regression risk, plugin-list hosts this grid. (3) `pnpm --filter @object-ui/plugin-grid type-check` -> EXIT=0 with the script line echoed ('> tsc --noEmit && tsc -p tsconfig.test.json'), so it genuinely ran rather than zero-matching a misspelled script. (4) `pnpm vitest run scripts/__tests__/{vitest-invocation-guard,turbo-test-inputs,vitest-config-alias-targets-3944}.test.ts` -> 109 passed. (5) eslint on both changed files -> '0 errors, 240 warnings', all pre-existing; my test file's 4 no-explicit-any warnings match the #5143 sibling's 5. (6) control-byte scan (grep -naP) on all changed files -> clean. NEW TEST packages/plugin-grid/src/__tests__/addRowCreatePermissionGate.test.tsx, 7 legs, modelled on the #5143 sibling inlineEditPermissionGate.test.tsx; probes the REAL ObjectGrid through usePermissions and asserts the user-visible outcome (is data-testid=add-record-row in the DOM), not the prop; the no-provider leg runs the REAL provider-less hook, not an imitation. Legs: a can+declared=shown; b cannot+declared=hidden; c declared-false/absent=hidden; d NO PermissionProvider=shown (designer path); e both directions agree; f object via data config gated; g no objectName untouched. REVERSE-VERIFICATION: baseline run first against the PRISTINE origin/main file, then formally repeated by restoring ObjectGrid.tsx from origin/main with the tests kept. PREDICTED BEFORE RUNNING: b, e, f red; a, c, d, g green. OBSERVED: exactly 'Tests 3 failed | 4 passed (7)', failures named b, e, f — match by name AND count, both times. Fix restored afterwards and confirmed byte-identical to the commit (`git diff HEAD --quiet` -> YES). BUILD ARTIFACT PER LEG: none sits between the edit and the thing under test on ANY leg — the edit is to packages/plugin-grid/src/ObjectGrid.tsx which the test imports as '../ObjectGrid', i.e. SOURCE not dist. The packages consumed through dist (@object-ui/components, which renders the add-record-row markup, plus core/permissions/react/fields) are untouched by this change and were built ONCE before the baseline via `pnpm --filter '@object-ui/plugin-grid^...' build` (EXIT=0); I proved the observable reached dist before any leg ran (grep -c add-record-row packages/components/dist/index.js -> 1). They are therefore fixed constants across every leg: no leg required a rebuild and none was skipped. Fail-open verified at source, not assumed: with no provider mounted usePermissions returns check: () => ({ allowed: true }), so can() answers true — that is what keeps the designer's add row alive, and leg d pins it.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Summary
Found while implementing #5143 (gating
ObjectGrid'seditablekey oncan(object, 'update')). Filed rather than fixed, per the scope rule — it is thecreateface of the same shape and was outside that PR's radius.Evidence (
packages/plugin-grid/src/ObjectGrid.tsx, ondfc697554)The Airtable-style inline "add record" row is gated on the author-declared operation alone:
operationsisschema.operationswhen declared, otherwise the{ update: !!onEdit, delete: !!onDelete }default — an authoring flag that says whether the affordance was wired, never a permission grant. Grepping the whole component for the principal's create verdict returns nothing:Contrast the two neighbours resolved a few lines apart at ~730, both of which ARE consumed:
permissionUpdate/permissionDeletefeedresolveRowCrudAffordances, and after #5143permissionUpdatealso feeds the inline-edit verdict. There is nopermissionCreateat all.Consequence, stated honestly
Same round-trip shape as #5143 and #4646: a principal with no
creategrant is offered the add-row, fills it in, and is stopped only by the server's 403. No data lands — the server gate is solid — but the UI walks the user through a write it knows will fail, while the toolbar's New button on the very same screen hides itself for that principal.Reachability today is the reason this is filed as an observation rather than a live defect, and the reason the label is
finding: nothing shipped declaresoperations: { create: true }on anobject-grid. Greppingpackages/,apps/andexamples/finds the key only in three tests, and all three set it tofalse. So the affordance is dormant unless an author writes the key — which is a real, declared authoring surface, and exactly the audience #5143 was about. Triage may well grade this higher than I have; the severity call is deliberately left to the triage round rather than guessed at filing time.Suggested shape
The conjunction the sibling gates already use, with the operation moved to
create:can()answerstruewith noPermissionProvider, so standalone embeds and the Studio designer keep today's behaviour — the fail-open every sibling gate in this file relies on. Whether the object's resolvedcreateaffordance (ADR-0103 bucket ∧userActions.create∧ the server's effective API operations) should join it, the way #5143 foldedisObjectInlineEditableinto the update verdict, is the design question worth settling before implementing —resolveCrudAffordancesalready producescreatePredicates(consumed by the related-list toolbar since #4646 / PR #5145), so there is a resolved answer available to AND with.Related (same family — "one affordance missing the gate its siblings have")
ObjectGrid'seditableschema key offers inline editing with nocan(object,'update')gate — the SDUI-authored grid path #4647 did not reach #5143 — theeditable/ inline-edit half of this very component.userActions.createpredicates — spec 17.0.0 shipped the declaration half, the renderer half is missing (createPredicatesproduced but never consumed) #4646 / Grid toolbar inline-edit toggle is not gated oncan(object, 'update');userActions.editInlineis declared in spec but has no consumer #4647 — the related-list "+ New" predicates and the ListView inline-edit toggle (PR fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (#4646, #4647) #5145).apiOperationsinstead of the caller's permission.requiredPermissions.