Repository navigation
Object list: New shows for a user without Create (auditor, read-only finance) and the save is then refused 403 #26193
Description
Filing gate ②: a maintainer ruling, carried out as child 7 of the nine that #26059's ruling (Q3) names. ⛔ Not a claim.
Users whose permission sets grant no Create still see the New button on object lists, for example an auditor (read + View All) or a read-only finance user. They can fill the form, and only the server's 403 tells them they could never save. The list is supposed to gate New on the effective permissions from /me/permissions. For this user set one input to that verdict answered "allowed", and the dev has to find which.
Part of #26059
Ruling-ref: 6416233738 (#26059 (comment))
Ruling: batch #396 item 1, letter Q1 是 / Q2 不做 / Q3 分拆 / Q4 框架. This card is Q3 child 7 (ui, p3): item 6. Brief 6415795834, item-by-item reading 6415971852 (item 6).
Filed by domain:ui seat 2 (domain:ui#2) · os-steve · session session_01SMps7wq7rBTk53e8KTSEup, first claimant of #26059. Domain, grade and route are triage's on first touch.
Business reason. Read-only roles (auditors, finance reviewers) are offered an action they can never complete, and they find out only after filling in a form. The server holds, so nothing leaks. The cost is wasted work and a console that contradicts the role it was configured for.
Reader who acts. The domain:ui execution seat, dispatch step: one os-dev, one PR on ui/packages/core/src/utils/affordanceGrants.ts and/or ui/packages/permissions/src/usePermissions.ts, whichever the reproduction blames, plus a pin.
Spec contract. None changes.
Duplicate check. A local scan of the 1,962 newest issues (all states, #22168–#26181) for listNew|New button.{0,120}(create|permission|Create)|(without|no) Create.{0,80}New found 0 hits.
Ruled scope (ruling Q3 item 7)
- New is hidden or greyed without Create.
- Reproduce with the auditor set and pin the
resolveAffordance('listNew')verdict (affordanceGrants.ts:293–310). - Suspected cause:
can()answering before/me/permissionsloads. The reading also names View All cascading into the create verdict as the other candidate.
Anchors (re-read on origin/main 83a26c38)
ui/packages/core/src/utils/affordanceGrants.ts:resolveAffordanceis at:293–306(the ruling's:293–310holds). Its grant check,perms.can(objectName, row.grant), is at:305.- The
listNewrow is at:147({ crud: 'create', grant: 'create' }). - The fail-open rule is documented at
:68–74: with no provider mounted,cananswerstrueandisLoadedfalse. resolveFieldAffordanceshort-circuits on!perms?.isLoadedat:320. The ruling had:321.
ui/packages/app-shell/src/views/ObjectDataPage.tsx:- The
listNewverdict (newVerdict = resolveAffordance('listNew', …)) is at:471–476. The ruling cited:503, which has drifted. Corrected. - The New button renders at
:567–575(objectCanCreate && createVisible).
- The
ui/packages/permissions/src/usePermissions.ts:115:usePermissions. Whether itscananswerstruewhile/me/permissionsis still loading is the open question. The reading did not read it line by line.- Effective API operations: objectstack#3391.
Expected / acceptance
- The reproduction with the auditor permission set is posted on this card, naming which input answered "allowed": the grant while loading, View All cascading, or the operation set.
- For a user without Create, New is not offered on the object list (hidden, or disabled with a reason), both after permissions load and while they are loading.
- A user with Create still sees New. Embeds with no permission provider keep today's fail-open behaviour; the server still enforces.
- A pinned test of the
resolveAffordance('listNew')verdict for the auditor set.
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade — as filed ·
pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T16:01Z⛔ Not a claim, ⛔ not a dispatch.
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 3 · 2026-10-26T16:39Z
Session:
session_01SMps7wq7rBTk53e8KTSEupAccount:
os-steveBranch:
claude/issue-26193-absent-create-bit-deniesWorktree:
objectstack-issue-26193Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/permissions/src/MePermissionsProvider.tsx(check:366–389 andcheckField's object-level fallback :359–362); tests inui/packages/permissions/src/__tests__/(update fixtures that relied on absent bits meaning allowed, add auditor / read-only-wildcard pins), plus oneresolveAffordance('listNew')pin; root.changeset/;scripts/sdui-manifest.sources/permissions.json(re-record, under the lock). ⛔affordanceGrants.ts/ui/packages/core(would move core.json, which #26011 also moves),ObjectDataPage.tsx, the server'seffective-object-permissions.ts,ui/.github/**, rootci.yml.Container & model:
M,mode:subagent,model: default tier,effort: highClause-②: yes (narrowing) — the console no longer offers Create / Edit / Delete when
/me/permissionsserves an object row that omits the bit. That is exactly when the server refuses it, because the spec grants only on=== true(permission.zod.ts:196, :218). The anonymous default and the no-provider fail-open are unchanged.Metadata-protocol: none
Responsibility:
the console's permission check, which reads an absent CRUD bit as allowed|@objectstack/spec's objectPermissionGrants (=== true) and the served /me/permissions rows|every user with a read-only or partial set (auditor, read-only finance) who is offered actions the server then refuses with 403Thread-read: 6420760374
Premise: corrected 2026-10-26T16:26Z — on
origin/maindf8aac41f2both suspected causes are ruled out by the code. (1) Loading: the provider renders only its fallback until data arrives (MePermissionsProvider.tsx:456), andcheckfails closed without data (:368). (2) View All does not cascade, becausecheckmapscreate → allowCreate(:373). What remains is the polarity::386 objPerm[k] !== false. The server's plain-wildcard coverage (coreeffective-object-permissions.ts:364–371) copies onlytruebits, so a read-only'*': { allowRead: true }set serves{ allowRead: true }with noallowCreate. The client allows it and the server refuses it. This is code diverging from spec, so the client aligns to the spec (protocol is the baseline), with no decision box.Serial constraints cleared: none — no open PR touches
ui/packages/permissionsorpermissions.json.Writes: PR only via the relay op
pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.=== true, incheckand incheckField's object-level fallback. Lowercase the object key incheckascheckFieldalready does.data.authenticated !== truefor anonymous and the no-providercan: () => true.'*'fixture giveslistNewallowed before the fix and denied after. Also pin Edit / Delete on the same fixture.affordanceGrantMap-12082(plugin-form) and the permissions suite. Any fixture that relied on absent-means-allowed gets explicit bits, and each flip is listed in the report.Fixes #26193.objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 26193, "status": "done", "branch": "claude/issue-26193-absent-create-bit-denies", "pr": "https://github.com/objectstack-ai/objectstack/pull/26273", "session": "session_01SMps7wq7rBTk53e8KTSEup", "premise_still_valid": true, "summary": "Confirmed the claim's corrected premise on origin/main df8aac41f2. Neither loading nor View All cascading is the cause. MePermissionsProvider read a served object row with bit !== false, while the server serves only true bits (a read-only '*': { allowRead: true } set is served as { allowRead: true }, an auditor as { allowRead, viewAllRecords }) and decides through the spec's objectPermissionGrants. check and checkField's object-level fallback now ask objectPermissionGrants (=== true plus the super-user bypasses, the server's own fold), and check lowercases the object key as checkField does. Anonymous (authenticated !== true), no-provider fail-open and unknown-object default are unchanged. Existing fixtures that needed explicit bits: none — all 176 test files reaching the provider passed unchanged.", "tests": "Repro at ccd94f7b, BASE provider restored into the tree (hash 9ab00a25 = base blob) then HEAD restored (6cf0d345 = HEAD blob) under trap: BEFORE 'Tests 15 failed | 57 passed (72)' (listNew/recordEdit/recordDelete allowed for read-only '*' and auditor, check allows every write action, checkField allows create/write, affordanceGrantMap auditor shape 4/4 red); AFTER 'Test Files 3 passed (3) · Tests 72 passed (72)'. Fixture sweep at 2dcfe6e5: 131 files mounting MePermissionsProvider/effectiveObjects 'Test Files 131 passed (131) · Tests 1666 passed (1666)'; 45 further files reaching /me/permissions 'Test Files 45 passed (45) · Tests 515 passed (515)'. At c95c1986: pnpm --filter @object-ui/permissions type-check and @object-ui/plugin-form type-check exit 0; vitest packages/permissions/ + the two plugin-form pins 'Test Files 15 passed (15) · Tests 212 passed (212)'. eslint over the 4 changed files exit 0. Closure build pnpm --filter '@object-ui/permissions^...' --filter '@object-ui/plugin-form^...' build: lock VERDICT command-exit 0. No ablation beyond the before/after (the before leg IS the provider-revert).", "gates": "Local at head 1c888a6e: dispatch-gates --commands derived 47; 46 exit 0 on first pass, pnpm check:type-check-debt exit 3 (PREREQUISITE NOT MET, lock queue) then re-run under the lock at 1c888a6e: exit 0 ('check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured, 26 raw tsc error(s) total, none above its recorded number'). --ran with recorded exit codes: '✓ 47 derived famil(ies) accounted for — 46 run, 1 NOT-MEASURED' (the NOT-MEASURED one is the first-pass type-check-debt, since measured green). node scripts/check-sdui-manifest.mjs exit 0 after re-recording permissions.json (1 of 27 records, artefact unchanged). PR check-runs on 1c888a6e: 61 complete, 54 success, 7 skipped, 0 failure — required: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core skipped, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Build & E2E UI success; Check Changeset success; Test UI 1-16/16 success.", "dogfood_battery": "1c888a6e · os-verify-lock: VERDICT command-exit 0 · held the lock 1661s · Test Files 297 passed | 3 skipped (300) · Tests 2527 passed | 18 skipped (2545) · no failures", "mcp_calls": "0", "api_writes": "2 — (1) relay pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches -> run 40375467200 executed POST /repos/objectstack-ai/objectstack/pulls (draft #26273) + POST /repos/objectstack-ai/objectstack/issues/26273/assignees [os-steve]; (2) os-dev-report comment via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/26193/comments. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — check('export') still maps to allowRead while the server's export grant is allowExport AND read; dormant: no production reader in ui/ calls can(object, 'export') (list/grid Export read the effective API operation set). In PR Acceptance notes." ], "deviations": "(1) One unlocked single-file vitest probe (~2 s) to read why the first locked run failed (packages/spec dist missing); everything else under os-verify-lock. (2) The PR body states dogfood and type-check-debt NOT MEASURED, because both were measured after the PR opened (green, above); the body is not PATCHed per os-dev — the seat may amend it. (3) PR footer follows AGENTS.md's single session-URL footer, not the harness's two-line attribution. (4) The listNew pin lives in ui/packages/plugin-form (beside affordanceGrantMap-12082), because @object-ui/permissions does not depend on @object-ui/core; plus a fifth 'auditor' shape in affordanceGrantMap-12082.test.tsx." }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Landed · seat 2 · 2026-10-26T19:55Z
b7279567, head1c888a6e(+294/−17, ACCEPT on #26273)origin/mainb3ed6c86, contains it (git merge-base --is-ancestorexit 0)gh-readonly-queue/main/pr-26273-*: none left (consumed)Fixes #26193on #26273Status: the console reads a served
/me/permissionsrow through the spec'sobjectPermissionGrants, so a user whose set omits Create, Edit or Delete (an auditor, a read-only'*'set) is no longer offered New, Edit or Delete that the server refuses. Anonymous and no-provider defaults are unchanged.pm:dispatchedremoved.