Repository navigation
Record form toast: a delegated-admin scope refusal shows the fixed no-permission line instead of the server's reason #26190
Description
Filing gate ②: a maintainer ruling, carried out as child 4 of the nine that #26059's ruling (Q3) names. ⛔ Not a claim.
When a delegated admin saves something outside their scope in Setup, the toast says only "You don't have permission to save this record." The specific reason the server gave is lost. Once child 3 gives that refusal its own code, the form should show the server's message for that code. Every ordinary permission denial keeps the fixed line.
Part of #26059
Ruling-ref: 6416233738 (#26059 (comment))
Blocked-by: #26189 (card 3 of this split, the server half; number filled in at filing)
Ruling: batch #396 item 1, letter Q1 是 / Q2 不做 / Q3 分拆 / Q4 框架. This card is Q3 child 4 (ui, p2): item 3, ui half. Brief 6415795834, item-by-item reading 6415971852 (item 3).
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. A delegated admin who hits their boundary needs to know which boundary it was (unit, allowlist or target user) to fix the assignment or ask the right person. The generic line sends them to a tenant admin for something they could correct themselves.
Reader who acts. The domain:ui execution seat, dispatch step, after the blocker lands: one os-dev, one PR on the shared refusal helpers in ui/packages/react/src/utils/error-message.ts and their two render sites.
Spec contract. The new code from child 3, as registered in the ADR-0112 ledger. No other contract.
Duplicate check. A local scan of the 1,962 newest issues (all states, #22168–#26181) for noPermissionToSave.{0,300}(delegat|reason|server message) and the reverse order found 0 hits. #26040 (a refused submit focuses the first invalid field) is adjacent, not this.
Ruled scope (ruling Q3 item 4)
- The toast renders that code's server message.
PERMISSION_DENIEDkeepsform.noPermissionToSave. Ruling A screen flow whose write node is refused shows the end user the engine's raw node text, in English, with the node id and the object API name ("Node 'create_account' failed: create_record(crm_account) failed: …") #24914 B′ is unchanged, so a denial never leaks why; only the delegation-scope code is shown.
Anchors (re-read on origin/main 83a26c38)
ui/packages/react/src/utils/error-message.ts::202isPermissionErrorholds. It answers true for any status 403 (:206), forPERMISSION_DENIED/FORBIDDEN(:208), and for "access denied" text (:209–214). A 403 carrying the new code would still be classed as a plain denial, so the new code has to be read before this check.:125–137declaredUserMessageis the existing producer-sideuserMessagemarking. It is read first at the form site.
ui/packages/components/src/renderers/form/form.tsx:3033–3044is the record-form toast:declaredUserMessage(error) ?? (isPermissionError(error) ? t('form.noPermissionToSave') : …). The ruling did not cite this site; it is where the reported toast comes from.ui/packages/app-shell/src/utils/flowResponse.ts:336–369holds. It is the same 24914 B′ rule on the flow-run path:permissionDeniedLineat:336–346, and the refusal arm at:369.form.noPermissionToSave:ui/packages/i18n/src/locales/en.ts:216.
Note for the dev (found while verifying, not a new rule). The ruled shape is the code. A server-side userMessage marking also exists (above). If the dev finds that the marking would serve instead, that is a question to ask on this card, linking the ruling. It is not a silent swap.
Expected / acceptance
- A save refused with child 3's code shows that refusal's server message in the toast and the in-form banner, on the record form and on the flow-run path that applies the same rule.
- A
PERMISSION_DENIED, a plain 403,FORBIDDENor a row-level-security denial still showsform.noPermissionToSave. - Tests cover both branches on both render sites.
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade — as filed ·
pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T16:00Z⛔ Not a claim, ⛔ not a dispatch.
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 1
Session:
session_017iXjUm1rujwDk7f3KT1Pkk(seat); implementing card threadsession_01JZRTRVa5FXXjuAJC6ZPM8PAccount:
os-steveBranch:
claude/project-thread-47phpr(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree: the card thread's own container
Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/react/src/utils/error-message.ts(read the delegation-scope code beforeisPermissionError),ui/packages/components/src/renderers/form/form.tsx(:3033–3044 toast),ui/packages/app-shell/src/utils/flowResponse.ts(:336–369), their tests (one per site plus a plain-403 control), locale keys only if the server message needs a fallback line, a root.changeset/patch, and the re-recordedscripts/sdui-manifest.sources/{react,components}.json. ⛔PERMISSION_DENIEDkeepsform.noPermissionToSave(ruling #24914 B′ unchanged) (stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root.changeset/.Container & model:
medium,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public ConsoleThread-read: 6420708732
Premise: holds (the blocker #26189 landed via #26320 on 2026-10-26T21:46Z, adding
DELEGATION_SCOPE_VIOLATION;origin/main:isPermissionErroratui/packages/react/src/utils/error-message.ts:202still classes any 403 as a plain denial; handed from seat 2 (#25995) by the split; 2026-10-26T22:53Z)Serial constraints cleared: react record: after #26292 (seat 1) and #26326 (seat 2); components record free since #26338 merged. Draft until the react line clears
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26190, "status": "done", "branch": "claude/project-thread-47phpr", "pr": "https://github.com/objectstack-ai/objectstack/pull/26363", "head": "2f86b3a3c33d1891cb3ebe3a3e8b64b3574c0944", "session": "session_01JZRTRVa5FXXjuAJC6ZPM8P", "premise_still_valid": true, "summary": "New exported delegationScopeMessage(err) in @object-ui/react answers the server reason (machine prefix stripped via extractWriteErrorMessage) when code or details.code is DELEGATION_SCOPE_VIOLATION, null otherwise. The record form asks it after declaredUserMessage and before isPermissionError (toast and banner share the value); flowFailureMessage asks it on the inner refusal before the permission check. PERMISSION_DENIED, FORBIDDEN, bare 403 and RLS keep form.noPermissionToSave (24914 B-prime unchanged). Premise holds for the form; on the flow path the inner refusal has no status and its text does not match isPermissionError, so the reason already rendered there with its [Security] prefix; the explicit read now makes both sites render the same string by code.", "tests": "head 2f86b3a3 (origin/main 222755eb merged): pnpm --dir ui exec vitest run --maxWorkers=2 packages/react/src/utils/ packages/components/src/__tests__/form-write-error-message.test.tsx packages/app-shell/src/utils/__tests__/flowResponse.test.ts -> Test Files 8 passed (8), Tests 145 passed (145), lock VERDICT command-exit 0. type-check (tsc --noEmit + tsconfig.test.json) for @object-ui/react, components, app-shell after turbo build of app-shell^... -> exit 0. Ablation on 43106bd8 via scripts/ablation-replace.mjs (src alias, no build needed): form.tsx line removed -> 1 failed | 10 passed, restored blob == HEAD; flowResponse.ts line removed -> 1 failed | 64 passed (diff: prefix [Security] reappears), restored blob == HEAD.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2f86b3a3: 45 commands, all exit 0 (incl. check-sdui-manifest, check:type-check-debt OK, check:cross-package-test-inputs, check:test-source-alias). --ran: 45 derived, 45 run, 0 NOT-MEASURED, 0 UNRUN. CI families (Lint & Repo Gates, Type Check UI, Test Core, Build & E2E UI ...): in_progress at report time, not re-run locally.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "line_budget": "code+tests+changeset+records: 10 files, about +200/-5", "files_changed": [ ".changeset/26190-delegation-scope-refusal-reason.md", "scripts/sdui-manifest.sources/components.json", "scripts/sdui-manifest.sources/react.json", "ui/packages/app-shell/src/utils/__tests__/flowResponse.test.ts", "ui/packages/app-shell/src/utils/flowResponse.ts", "ui/packages/components/src/__tests__/form-write-error-message.test.tsx", "ui/packages/components/src/renderers/form/form.tsx", "ui/packages/react/src/index.ts", "ui/packages/react/src/utils/error-message.test.ts", "ui/packages/react/src/utils/error-message.ts" ], "deviations": [ "origin/main merged once before the PR opened (it had moved error-message.ts's import and react.json); the import conflict was resolved by keeping both imports, react.json was taken from main and regenerated, not hand-merged. Three later main commits not merged (no conflict reported).", "No PR activity subscription taken: the seat drives CI and landing per the standard order." ], "mcp_calls": "3 — mcp__github__issue_read x3 (get, get_comments x2); no write tools", "api_writes": "3 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, POST /issues/26363/assignees) and this os-dev-report comment (POST /issues/26190/comments); git push is not a REST write", "open_questions": [ { "question": "delegationScopeMessage is a new public export of @object-ui/react (additive). The claim declared Clause-②: no and it is copied as claimed; does the seat want it re-declared as yes (widening)?", "options": [ "A keep Clause-②: no (an added renderer helper, no spec or wire contract moves)", "B re-declare Clause-②: yes (widening) on the PR body and changeset" ], "recommendation": "A, because nothing in @objectstack/spec, the wire or the metadata an author writes moves; the export is a renderer-internal helper consumed by two sibling ui packages" } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim amendment (seat
domain:ui#1, 2026-10-27T00:03Z): amends the claim6429244850. Append-only; its other lines stand.Clause-②: yes (widening) — the PR (#26363) adds an exported
delegationScopeMessageto@object-ui/react's entry, which enlarges the public surface. Nothing narrows.