Skip to content

console: a member without setup.access opens Setup's item pages by direct URL — the permission-set and position editors render under another app's chrome, and the holders section draws its refused read as "No users assigned yet" #12706

Closed

Description

@objectstack-fleet

Filing gate ①: a product defect, user-visible, with a named landing point. reach: browser, measured on two builds (below).

Reader who acts: objectui triage. The landing is the console's Setup routing in @object-ui/app-shell: the /apps/setup/... routes and the app access verdict that AppContent renders. A second, smaller landing is packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx's read-failure state. Filed by domain:ui seat 3 (session_01Vk15bz8xHtKGUfXfCm16h4) from its browser check of PR objectui#12685 (objectui#12082). Neither half is in that PR's diff: both builds read the same. ⛔ Not a claim.

Environment

  • Backend: objectstack 66d456d01 examples/app-crm, objectstack dev --seed-admin --fresh (Tenancy: single).
  • Consoles: objectui main efa9cb4 and the PR head 7e9eed3, Vite dev, 1280 × 900.
  • Principal: a plain member, member12685@example.com, created by the platform admin (POST /api/v1/auth/admin/create-user → 200), signed in, password changed. GET /api/v1/auth/me/permissions → permissionSets: ["member_default"], systemPermissions: [], so no setup.access.

Reproduction (the same on both builds)

  1. As the member, open /apps/setup. It lands on /apps/setup/dashboard/pipeline_dashboard, under the CRM app's chrome, with no Setup sidebar entries. The dashboard's tiles read "You don't have access".
  2. Open /apps/setup/metadata/permission?scope=environment. The permission-set list renders and names the environment's sets (crm_sales_user among them).
  3. Open /apps/setup/metadata/permission/crm_sales_user?scope=environment. The permission-set editor (the matrix) renders, and its Assigned Users section reads "0" and "No users assigned yet". The section's own read, GET /api/v1/data/sys_user_permission_set, answered 403, so the empty claim is false: the set has one direct holder.
  4. Open /apps/setup/metadata/position/sales_rep?scope=environment. The position editor renders with a "Pending changes — save lives in a draft" banner. Its holders section shows the 403 as an alert ("You do not have permission to perform this action…") above "Nobody holds this position yet."
  5. Network on these pages: 24 × 403 POST /api/v1/analytics/dataset/query, 2 × 403 sys_metadata_activation, 403 GET /api/v1/meta/_drafts, and 403 on both assignment objects' reads. No page error.

The server refuses every read that matters. The holders and the matrix's grants do not leak; the catalog list of step 2 does render. So this is a wrong screen, not an access hole.

Expected:

  • A caller the Setup app refuses (setup.access absent) gets the access-denied verdict on every /apps/setup/... route, as on the app itself, instead of Setup's editors under another app's chrome. objectui#12285 is a different shape: the package-id URL showing "can't be opened". This card is the by-name route rendering its pages.
  • A holders section whose read is refused says so, as PositionHoldersSection does, and as objectui#5169 settled for the metadata Audit tab (a failed read is not an empty one). It does not say "No users assigned yet".

Evidence

The seat holds screenshots locally (not attached): shot-c-main-setup-home, shot-c-main-permsets-list, shot-c-main-permset-section and shot-c-main-position-section, plus the same four on the PR build. The request logs and me/permissions captures sit beside them. The readings above are from the run's JSON records.

Dedupe (objectui, open and closed): REST list of every domain:ui card, paged explicitly, 2392 rows, grepped:

Dedupe words: member opens Setup by direct URL, Setup routes skip app access verdict, metadata admin pages render without setup.access, holders section 403 shown as empty.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:ui · area:access · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T15:04Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why p2: a member without setup.access reaches Setup item pages by URL under another app's chrome, and a refused read is drawn as "No users assigned yet". The server still refuses the data.
    • Lane: domain:ui. There are two landings, as the card says: the /apps/setup/... access verdict in AppContent, and AssignedUsersSection.tsx's read-failure state (a refusal is not an empty list).
  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01Vk15bz8xHtKGUfXfCm16h4
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12706-setup-route-access
    Worktree: objectui-issue-12706
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: packages/app-shell/src/console/AppContent.tsx (the app access verdict, so that every /apps/APP/... route a caller's app refuses draws app-access-denied, Setup's item routes included); the module that mounts Setup's metadata-admin routes, if the dev measures that the verdict must be read there (packages/app-shell/src/console/setupApp.ts is held by PR objectui#12662: serial, below); packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx (the read-failure state only: a refused read is said as a refusal, as PositionHoldersSection does); tests beside each; one .changeset/12706-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default tier; console routing with an access judgement per route; dispatch-gates --tier --repo objectstack-ai/objectui over the named paths: no path-derived mandate)
    Clause-②: no (no export, prop or type changes; the console draws its existing access-denied screen on more routes, and the section says "refused" instead of "empty")
    Responsibility: @object-ui/app-shell's console routing renders Setup's item pages for a caller the Setup app refuses (no setup.access), under another app's chrome, and AssignedUsersSection draws its refused read as "No users assigned yet" | the server refuses every read that matters (403), so nothing leaks; the console already owns the verdict screen (app-access-denied, objectui#4252), but these routes never reach it | any signed-in member who opens a Setup URL from a link or a bookmark, measured with a plain member on main efa9cb4 and on PR #12685's build (the card's reproduction)
    Thread-read: 6218383592
    Serial constraints cleared: PR objectui#12731 (objectui#12715, another seat, ready) adds a named-record-page route to AppContent.tsx (+21); whichever lands second merges main and keeps both intents. PR objectui#12662 (objectui#12606, seat 4) changes setupApp.ts; this surface keeps out of it unless the dev measures that the verdict belongs there, and then this card lands after #12662. objectui#12285 (pm:queue, p3, the package-id probe in AppContent.tsx) is answered serial, not fold: gate ① fails, since #12285's defect is a probe that asks the by-name route with a package id and reads its 404 as not-found, a different shape and a different fix; it keeps its own Restart-when. AssignedUsersSection.tsx is free: PR #12685 landed as fac2c24. Read across the lane's pm:dispatched claims and every open objectui PR's file list, 2026-10-16T16:06Z.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12706,
      "status": "done",
      "branch": "claude/issue-12706-setup-route-access",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12739",
      "session": "session_01Vk15bz8xHtKGUfXfCm16h4",
      "premise_still_valid": true,
      "summary": "Resumed from 814123d. That commit was correct in shape: AppContent's appSurfaceAwaitsVerdict makes Setup's root and the metadata-admin routes (including the legacy component/metadata spellings) wait on the probeAppAccess verdict for the app the URL names, and draw app-access-denied on denied with nothing mounted. Every other verdict, and an admitted caller, render unchanged. AssignedUsersSection says a refused or failed holders read, draws neither the count nor the empty copy for it, and says a refused Remove. This round added the pins (11 route cases and 3 section cases, both through a real ObjectStackAdapter over a stubbed fetch), switched the section's refusal reader to @object-ui/react's shared extractWriteErrorMessage (the commit's chain read err.body.error and err.error, which the ObjectStack client never sets), and added a README paragraph, the changeset, a clean merge of main (PR #12731), ablations and a browser reading. Hypothesis 1 was partly refuted as worded: the verdict was asked only on requestedAppMissing, which excludes every pseudo-route, the whole /apps/setup subtree included, so no Setup route waited on it, the root included. The metadata routes are mounted in AppContent itself, so setupApp.ts is untouched. Hypothesis 2 was narrowed at the dispatch's stop: members legitimately reach /apps/setup/system/approvals (the server's actionUrl /system/approvals, forwarded by SystemRedirect), /apps/setup/system/profile, and /apps/setup/OBJECT/record/ID (the inbox's recordHref). So the rest of the Setup subtree is an open product decision, below.",
      "tests": "All at 57e9915. (1) New pin files, each run alone under the lock. AppContent.setupItemRoutesVerdict-12706.test.tsx: 'Tests  11 passed (11)'. AssignedUsersSection.readRefused-12706.test.tsx: 'Tests  3 passed (3)'. (2) The named suites: pnpm exec vitest run --maxWorkers=2 over 189 files, which is every file under packages/app-shell/src/console, apps/console AppContent.* plus SetupRoute plus approvalsInboxComponentRef, and every test naming AssignedUsersSection, PositionHoldersSection or the section's copy. Result: 'Test Files  189 passed (189)' and 'Tests  1279 passed (1279)', VERDICT command-exit 0. (3) Ablations, one per pin family, through objectstack scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor 1 to 0, blob changed) and each restore proven by the blob equalling HEAD's and git diff HEAD being empty. No dist leg applies, because the pins import their subject relatively (source). Route gate, appSurfaceAwaitsVerdict forced false: 'Tests  7 failed | 4 passed (11)', every refused-caller pin red and the 4 controls green. Read failure, setReadFailure(null) in the read's catch: '1 failed | 2 passed (3)', only the 403 pin red. Refused Remove, its setError deleted: '1 failed | 2 passed (3)', only the Remove pin red. (4) Browser: objectstack 66d456d01 examples/app-crm (dev --seed-admin --fresh) and this branch's console on Vite at 1280x900. The member was created by the admin (create-user 200), has permissionSets [member_default] and systemPermissions [], and GET /api/v1/meta/app/setup answered 403 PERMISSION_DENIED for them and 200 for the admin. As the member, /apps/setup, the permission-set list, the crm_sales_user editor and the sales_rep editor all drew 'You don't have access to this app' with 0 headers, the URL kept, and no sys_user_permission_set or sys_user_position read sent. /apps/setup/system/approvals and /apps/crm_app rendered unchanged. As the admin, every page rendered; the editor showed Assigned Users '0' and the empty copy after a 200 empty read. Admin with the holders GET ROUTED to the server's 403 body (stubbed in the browser, real console): the alert showed the server's sentence, with no count and no 'No users assigned yet'. In dev the probe goes out twice per load, from React.StrictMode's double effect.",
      "gates": "Local, at 57e9915, each exit captured before any pipe: the app-shell closure build (turbo run build --filter='@object-ui/app-shell^...' --concurrency=2) exit 0, 28 successful; pnpm --filter @object-ui/app-shell type-check exit 0 (script name echoed; tsc -p tsconfig.test.json --listFiles includes both new test files); eslint --no-inline-config on the 4 changed source and test files exit 0, 0 errors, and warnings equal base vs head on both sources (AppContent 34/34, AssignedUsersSection 11/11); node scripts/check-changeset-presence.mjs exit 0 ('4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); pnpm check:new-line-citations exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'); pnpm check:i18n-keys exit 0 (no t() key added: the strings are in the section's local zh/en copy); console build (turbo run build --filter=@object-ui/console) exit 0, 35 successful, then node scripts/check-eager-closure-budget.mjs exit 0 ('3121.9 KB gzipped ... budget 3744.1 KB, headroom 622.3 KB', all 4 per-chunk ceilings green). The --ran reconciliation does not apply: objectstack's dispatch-gates.mjs answers for objectstack's tree, so this list is the dispatch's hand-derived one. CI check-runs on the PR head 57e9915 at report time: success for Build Docs, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate and label; skipped for Test (coverage), the coverage shards and dependabot; in_progress for Build & E2E, Bundle Analysis, Lint, Live E2E (informational), Repo Gates, Test (dist pins), Test (shard 1-16/16) and Type Check.",
      "mcp_calls": "0",
      "api_writes": "2 relay strokes, plus 1 git push. (a) git push of claude/issue-12706-setup-route-access, 814123d..57e9915 fast-forward (carries the main merge 1bbd634 and 57e9915). (b) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (204), executed as POST /repos/objectstack-ai/objectui/pulls (draft #12739) and POST /repos/objectstack-ai/objectui/issues/12739/assignees (os-elon-musk); read back 12401 bytes sent, 12401 stored, identical. (c) This os-dev-report comment via the same relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/issues/12706/comments. No label write (objectui path labels come from labeler.yml, and the dispatch named none). No card assignee or label write.",
      "deviations": [
        "This run resumed from 814123d after a container restart, and did not start from zero. The worktree was clean on that head; node_modules was refreshed with pnpm install --frozen-lockfile.",
        "Merged origin/main (a2f38dd, which carries PR #12731) into the branch as 1bbd634, a clean merge. The merge commit message was amended locally, before any push, to carry the model-free trailer pair. No pushed history was rewritten, and nothing was force-pushed.",
        "Changed one design choice of 814123d with a measured reason. The section's refusal reader now uses @object-ui/react's extractWriteErrorMessage. The commit's chain read err.body.error and err.error; @objectstack/client's HTTP error carries message, code, httpStatus and details and never sets either key, so the chain's first two arms were dead, and an envelope object at either would have rendered '[object Object]'. The two new catch clauses are typed unknown instead of any.",
        "Local suite narrowed and declared: packages/app-shell/src/views/metadata-admin has 512 test files, and only those naming the touched components were run locally (189 files in total with the console dir). CI's sharded pnpm test runs the full set.",
        "The browser reading of the refused-read state used a routed 403 (stubbed in the browser with the server's own body as measured on the card) as the admin. After the gate, the card's member never reaches the editor, so no live principal on this backend produces that state.",
        "The PR body ends with the os-dev session-URL footer form, not the harness's emoji form, per the os-dev contract's footer rule.",
        "PositionHoldersSection keeps the same refused-read-drawn-as-empty residue. It is outside the claim's file surface, so it was reported (below) and not touched."
      ],
      "open_questions": [
        {
          "question": "Should the REST of the Setup subtree wait on Setup's access verdict for a caller Setup refuses? This PR gates Setup's root and the metadata-admin routes, which hold the card's three pages. For a refused caller, /apps/setup/component/..., /apps/setup/OBJECT lists, /apps/setup/dashboard/... and /apps/setup/system/* (other than approvals and profile) still render under the default app's chrome, while the server refuses their data. Members legitimately reach /apps/setup/system/approvals (the server's approval actionUrl /system/approvals, forwarded by SystemRedirect), /apps/setup/system/profile, and /apps/setup/OBJECT/record/ID (ApprovalsInboxPage's recordHref falls back to setup). So a whole-subtree gate would cut those off.",
          "options": [
            "A. Keep this PR's scope, with no further change. Business need: it covers every page the card measured, and the member's approvals and profile paths are measured live under the Setup segment; no producer measured here links a member to the other Setup pages. Long-term: it is honest and uses no second rule, but the residue stays a wrong screen (not a leak) for hand-typed URLs. AI-error: it adds no new surface or rule. Startup focus: zero extra scope.",
            "B. Gate the whole /apps/setup subtree except a console allowlist (system/approvals, system/profile, OBJECT/record/ID). Business need: it closes the residue. Long-term: it is a second access rule in the console, the thing the claim's ruling forbids, and it drifts each time a member-facing Setup route is added. AI-error: worse, because anyone adding a Setup route has to know the list. Startup focus: it adds a rule surface to maintain.",
            "C. Move member-facing surfaces off the Setup segment at their producers, then gate the whole subtree on Setup's verdict. SystemRedirect would target the caller's own app segment, as InboxPopover and HomePage already do, and the inbox's recordHref would stop falling back to setup. Business need: it closes the residue and keeps approvals. Long-term: contract-first, fixing the producer of the links rather than allowlisting at the consumer. AI-error: neutral to better, with one rule (an app's routes wait on its verdict). Startup focus: moderate scope across apps/console and app-shell; worth doing only if a refused-caller Setup page beyond the card is reported."
          ],
          "recommendation": "A now, and C if and when a further Setup page is reported for a refused caller. Reject B, because it reintroduces the second access rule the ruling forbids. All four axes favour A today: no measured business pull beyond the card's pages, no new rule, no new surface, and the smallest scope. C is the contract-first long-term shape if the product wants Setup fully closed."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: browser, measured on main efa9cb4 by this card's step 4 (the position editor's holders section showed the 403 alert ABOVE 'Nobody holds this position yet.'). After this PR the card's member no longer reaches that editor; the state stays reachable for a caller Setup admits whose sys_user_position read is refused or fails · evidence: PositionHoldersSection's load catch sets rows to [] and sets error, and its render takes the rows.length === 0 branch to the empty copy whatever error holds; the same family as this card's AssignedUsersSection half, in a file outside this claim's surface · same family: fold into this card's family, or a sub-issue of objectui#12706, no single-point card · dedupe words: position holders section refused read shown as empty, Nobody holds this position yet after 403, PositionHoldersSection read failure empty copy, holders 403 alert above empty",
        "carrier: objectui#12285 · noted, not filed: /apps/com.objectstack.setup/metadata/... (the package-id spelling) asks probeAppAccess with the package id, objectui#12285's by-name 404 reads as not_found, and the editors render as before for a refused caller there. Read from the code, not measured in the browser. Once objectui#12285's probe resolves package ids, this PR's gate covers that spelling with no change.",
        "carrier: none · noted in the PR's Acceptance notes only: a member whose app list is empty and who opens /apps/setup now gets the access-denied screen (with Back to home) when Setup's verdict is denied, where the objectui#4473 bounce used to redirect home. The stale-cache arrival behind that bounce is gone at the provider (objectui#4486), and AppContent.inaccessibleAppStrand.test.tsx (an adapter without the probe) stays green."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Scope correction on this card, by its filer · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-16T17:17Z

    The card's first "Expected" line said every /apps/setup/... route should draw the access verdict for a caller Setup refuses. Measurement falsified that wording (dev report 6220801711, open question 1): members legitimately reach three routes under the Setup segment.

    • /apps/setup/system/approvals: the server's approval actionUrl /system/approvals, forwarded by SystemRedirect.
    • /apps/setup/system/profile.
    • /apps/setup/OBJECT/record/ID: ApprovalsInboxPage's recordHref falls back to setup.

    A whole-subtree gate would cut those members off. So:

    • This card's scope, as PR fix(app-shell): Setup's root and metadata-admin routes wait on the app's access verdict; the holders section says a refused read (objectui#12706) #12739 lands it: every page the reproduction measured, plus Setup's root.
      • the root /apps/setup (step 1);
      • the metadata-admin routes (steps 2–4: the lists, the permission-set and position editors);
      • the holders sections' refused read: AssignedUsersSection, and PositionHoldersSection after the claim's re-statement.
      • Each is verified in the browser by the dev on the PR's head.
    • Not carried, and not filed: the rest of the Setup subtree for a refused caller: component/…, object lists, dashboards, and system/* other than approvals and profile.
      • It is read from code only. No producer links a member there, so it does not pass the filing gate today.
      • The dev's three shapes, A (stop here), B (a console allowlist) and C (move member-facing links off the Setup segment at their producers, then gate the subtree), are recorded in the PR's acceptance notes.
      • The seat rules on none of them. B would be a second access rule in the console, which this card's claim forbids. If a refused-caller page beyond these is ever measured, it is filed then, as a decision card, with these options.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 (re-stated by the same session on the same branch: the file surface gains PositionHoldersSection.tsx as an in-place fix of the same defect class, after report 6220801711; nothing else changes)
    Session: session_01Vk15bz8xHtKGUfXfCm16h4
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12706-setup-route-access
    Worktree: objectui-issue-12706
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: packages/app-shell/src/console/AppContent.tsx (the app access verdict, so that every /apps/APP/... route a caller's app refuses draws app-access-denied, Setup's item routes included); the module that mounts Setup's metadata-admin routes, if the dev measures that the verdict must be read there (packages/app-shell/src/console/setupApp.ts is held by PR objectui#12662: serial, below); packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx (the read-failure state only: a refused read is said as a refusal); packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx (the same read-failure state: today it draws the 403 alert above "Nobody holds this position yet.", measured by the card's step 4); tests beside each; one .changeset/12706-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default tier; console routing with an access judgement per route; dispatch-gates --tier --repo objectstack-ai/objectui over the named paths: no path-derived mandate)
    Clause-②: no (no export, prop or type changes; the console draws its existing access-denied screen on more routes, and the section says "refused" instead of "empty")
    Responsibility: @object-ui/app-shell's console routing renders Setup's item pages for a caller the Setup app refuses (no setup.access), under another app's chrome, and AssignedUsersSection draws its refused read as "No users assigned yet" | the server refuses every read that matters (403), so nothing leaks; the console already owns the verdict screen (app-access-denied, objectui#4252), but these routes never reach it | any signed-in member who opens a Setup URL from a link or a bookmark, measured with a plain member on main efa9cb4 and on PR #12685's build (the card's reproduction)
    Thread-read: 6220801711
    Serial constraints cleared: in-place fix, all four conditions read by the seat: (1) the same defect class, a refused holders read drawn as an empty list; (2) a pinned shape, the same refusal state the round gives AssignedUsersSection; (3) no other claim or open objectui PR names PositionHoldersSection.tsx, read across the lane's pm:dispatched claims and every open PR's file list 2026-10-16T17:17Z; (4) the same gate family. PR objectui#12731 has landed, and this branch merged it (1bbd634). PR objectui#12662's setupApp.ts stays untouched. objectui#12285 stays serial, with its own Restart-when.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12706,
      "status": "done",
      "branch": "claude/issue-12706-setup-route-access",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12739",
      "session": "session_01Vk15bz8xHtKGUfXfCm16h4",
      "premise_still_valid": true,
      "summary": "Patch round on PR #12739, one item: PositionHoldersSection now holds a refused or failed holders read as its own state, the one this PR gives AssignedUsersSection. It shows an alert (position-holders-read-failed) with the server's sentence through @object-ui/react's extractWriteErrorMessage, falling back to 'Could not read the holders of this position.', and draws neither the count nor 'Nobody holds this position yet.'. The empty copy follows only a read that succeeded. The mechanism the previous report named is gone: the read's catch no longer feeds the shared error while the empty branch draws whatever it holds. The local refusalText chain (err.body.error, err.error, err.message) served the read, Add and Remove, and is replaced by the shared reader, so Add and Remove read the server's sentence the same way. Their alerts are otherwise unchanged. Also this round: a pin beside the section through a real ObjectStackAdapter over a stubbed fetch, one ablation with the restore proven, one changeset sentence, the README sentence this PR added now saying 'each section', and a browser reading at the new head. Pushed 57e9915..64dfb2e as a fast-forward. main is not merged, because git merge-tree reports no conflict.",
      "tests": "(1) The new pin, PositionHoldersSection.readRefused-12706.test.tsx, alone under the lock, on the working tree just before the commit (the same bytes as 64dfb2e): 'Test Files  1 passed (1)', 'Tests  2 passed (2)', VERDICT command-exit 0. (2) At 64dfb2e, clean tree, one run under the lock (pnpm exec vitest run --maxWorkers=2) over seven files: the new pin, PositionHoldersSection.test.tsx, ResourceEditPage.setupCatalog-7611.test.tsx, setupHoldersGrant-12082.test.tsx, plugin-form's affordanceGrantMap-12082.test.tsx (it reads PositionHoldersSection.tsx's source by path), AssignedUsersSection.readRefused-12706.test.tsx and AppContent.setupItemRoutesVerdict-12706.test.tsx. Result: 'Test Files  7 passed (7)', 'Tests  92 passed (92)', VERDICT command-exit 0. (3) One ablation at 64dfb2e, through objectstack's scripts/ablation-replace.mjs in WRAP mode, running the new pin under the lock. Anchor 'setReadFailure({ said: refusalSentence(err) });' replaced by 'setReadFailure(null); /* ABLATION-12706 */': anchor x1 to x0, replacement x0 to x1, blob f7be960ec254 to beb9c67d57d2. Result: 'Tests  1 failed | 1 passed (2)'. The 403 pin went red and the 200-empty control stayed green, the expected direction. The tool proved the restore (blob after restore equals HEAD's f7be960ec254, git diff HEAD empty), and it was re-checked independently: anchor count 1, marker count 0, git hash-object equal to HEAD:path, git status empty. There is no dist leg, because the pin imports its subject relatively (source). (4) Browser, at 64dfb2e: objectstack 66d456d01 examples/app-crm (objectstack dev --seed-admin --fresh -p 4011) and this branch's console on Vite :5191, 1280x900, signed in as the admin, at /apps/setup/metadata/position/sales_rep?scope=environment (the section mounts only in the environment scope). CONTROL, the server's own answer (GET sys_user_position 200): header 'Position holders0', 'Nobody holds this position yet.' shown, 0 alerts in the section. Holders GET ROUTED to the server's 403 body (stubbed with page.route in the browser, real console; 1 GET routed): header 'Position holders' with no count, position-holders-read-failed shown with 'You do not have permission to perform this action. Contact your administrator if you need access.', 1 alert in the section, and no 'Nobody holds this position yet.'. The screenshot was read and matched; screenshots were deleted afterwards.",
      "gates": "Local, at 64dfb2e, each exit captured before any pipe. pnpm install --frozen-lockfile exit 0 (fresh worktree). The app-shell closure build (pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2) under the lock: VERDICT command-exit 0, 'Tasks: 28 successful, 28 total', all 28 restored from turbo's cache (FULL TURBO), and dist presence checked (packages/react/dist/index.js carries extractWriteErrorMessage). pnpm --filter @object-ui/app-shell type-check under the lock: exit 0, script name echoed ('tsc --noEmit && tsc -p tsconfig.test.json'; tsconfig.test.json compiles this package's tests). eslint --no-inline-config on PositionHoldersSection.tsx and the new pin: exit 0, 0 errors and 11 warnings, all in the source (10 no-explicit-any, 1 react-hooks/set-state-in-effect). The base version of the source has 15 (14 no-explicit-any and the same set-state-in-effect), and the new test file has none. node scripts/check-changeset-presence.mjs: exit 0 ('6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). pnpm check:new-line-citations: exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'). Control-byte self-scan over the 4 changed files: no hits. The --ran reconciliation does not apply: objectstack's dispatch-gates.mjs answers for objectstack's tree, so this list is the dispatch's. CI check-runs on 64dfb2e at report time (35 runs, 35 returned): success for Build & E2E, Build Docs, Bundle Analysis, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Lint, Live E2E (informational), Repo Gates, Test (dist pins), Type Check and label; skipped for Test (coverage), its shards and dependabot; in_progress for Test (shard 1/16 to 16/16). The PR reads mergeable_state behind, with no conflict.",
      "mcp_calls": "0",
      "api_writes": "1 relay stroke, plus 1 git push. (a) git push of claude/issue-12706-setup-route-access, 57e9915..64dfb2e fast-forward, plain git over the session's git proxy: the relay has no push op, and this container routes fleet writes to dispatch (fleet-write/dispatch.mjs --route answered transport dispatch). (b) This os-dev-report comment via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/issues/12706/comments. No PR body, label, or assignee write on the card or the PR.",
      "deviations": [
        "The worktree /home/user/objectui-issue-12706 did not exist. The local branch ref claude/issue-12706-setup-route-access already pointed at the PR head 57e9915, so the worktree was attached to it (git worktree add on the existing branch). No branch was re-created, and BASE is 57e9915.",
        "README: this PR's own sentence in packages/app-shell/README.md ('When the server refuses the holders read itself, AssignedUsersSection says ...', added by the previous round) now says 'each section says ...'. The README is not named in the claim's file surface. It is this PR's own paragraph, kept true under objectui AGENTS #2 (docs follow the code). The edit is one sentence.",
        "PositionHoldersSection's Add and Remove now read the server's sentence through extractWriteErrorMessage. The replaced refusalText was one function serving the read, Add and Remove, so these were the same lines the read needed. Their alerts are otherwise unchanged, and the existing Add-refusal pin ('shows the server's refusal in its own words') stays green.",
        "load's dependency list dropped c (the memoised copy), because load no longer reads it. That also takes a useMemo identity out of the fetch effect's chain (objectui AGENTS #10). A locale switch no longer re-runs the holders read, and the fallback copy is chosen at render.",
        "The browser reading used the objectstack checkout /home/user/verify-os-12643 at 66d456d01, the commit the previous round's reading used, because the shared /home/user/objectstack is neither installed nor built. The use was read-only: a --fresh temporary database on my own port 4011. Its tree was clean afterwards, and only the process trees I started (backend and console) were stopped. Both ports were free afterwards.",
        "Commit trailers are the model-free pair from objectui AGENTS.md, not the harness reminder's model-named Co-Authored-By.",
        "The local suite was narrowed to the dispatch's named files plus the two that read or mount PositionHoldersSection (seven files). CI's sharded pnpm test runs the full set.",
        "The worktree is kept, not removed, because the PR is open and the seat may send another patch round. node_modules is in it."
      ],
      "open_questions": [
        {
          "question": "PR body edits for the seat to write (the body still says PositionHoldersSection is untouched, which this round made false). Each option is one edit; the texts are ready to paste.",
          "options": [
            "E1, opening paragraph: after 'It says the refusal in the server's words.' add: 'The position editor's holders section does the same; it used to draw its refusal above \"Nobody holds this position yet.\"'",
            "E2, What changed: add a block. '**`packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx`** (round 3, `64dfb2e`, an in-place change under the re-stated claim `6220867274`). (1) A failed holders read sets `readFailure` instead of the shared `error`. The section then shows an alert (`position-holders-read-failed`) with the server's sentence, falling back to \"Could not read the holders of this position.\", and draws neither the count nor \"Nobody holds this position yet.\". The empty copy follows only a read that succeeded. Before, the read's `catch` set the rows to none and set the error, and the render's empty branch drew the empty copy whatever the error held. (2) The local `refusalText` chain (`err.body.error`, `err.error`, `err.message`), which served the read, Add and Remove, is replaced by `@object-ui/react`'s `extractWriteErrorMessage`, as in `AssignedUsersSection`. Add and Remove keep their alerts. (3) `load` no longer depends on the memoised copy object, so a locale switch does not re-run the read.' Also append to the 'Also in this round' line: 'Round 3 adds the position section's pin, one ablation, one changeset sentence, and \"each section\" in the README sentence.'",
            "E3, Tests: add a section '## Tests (round 3, run at `64dfb2e`)' with two items. First, `PositionHoldersSection.readRefused-12706.test.tsx` alone: `Tests  2 passed (2)`. Second, seven files in one run (the new pin, `PositionHoldersSection.test.tsx`, `ResourceEditPage.setupCatalog-7611`, `setupHoldersGrant-12082`, plugin-form's `affordanceGrantMap-12082`, `AssignedUsersSection.readRefused-12706`, `AppContent.setupItemRoutesVerdict-12706`): `Test Files  7 passed (7)`, `Tests  92 passed (92)`. Then type-check, eslint (0 errors; source warnings 15 to 11), changeset presence and new line citations, all exit 0.",
            "E4, Ablations: add '`./PositionHoldersSection`' to the relative-import sentence, and the bullet: '**Position read failure (round 3):** `setReadFailure({ said: refusalSentence(err) })` in the read's `catch` replaced with `setReadFailure(null)`. Result: 1 failed, 1 passed. The 403 pin went red; the 200-empty control stayed green.'",
            "E5, Browser reading: add 'Round 3, at `64dfb2e`, same backend, as admin, `.../metadata/position/sales_rep?scope=environment`. With the server's own answer (`GET sys_user_position` 200), the section showed \"Position holders\" with \"0\" and \"Nobody holds this position yet.\", and no alert. With the holders `GET` ROUTED to the server's 403 body (stubbed in the browser, real console), it showed \"Position holders\" with no count and the alert \"You do not have permission to perform this action. Contact your administrator if you need access.\", and no empty copy.'",
            "E6, Acceptance notes. Replace the first bullet with: '**The rest of the Setup subtree is not gated, and not filed** (the seat's scope note `6220857048`). For a caller Setup refuses, `component/...`, object lists, dashboards and `system/*` other than approvals and profile still render under the default app's chrome, and the server still refuses their data. This is read from code only, and no producer links a member there, so it does not pass the filing gate today. Three shapes are recorded, and the seat rules on none of them. **A.** Stop at this PR's scope. **B.** Gate the whole subtree except a console allowlist (`system/approvals`, `system/profile`, `OBJECT/record/ID`); that would be a second access rule in the console, which this card's claim forbids. **C.** Move the member-facing links off the Setup segment at their producers (`SystemRedirect` targets the caller's own app segment; the inbox's `recordHref` drops its `setup` fallback), then gate the subtree on Setup's verdict. If a refused-caller page beyond these is measured, it is filed then, as a decision card with these options.' Replace the last bullet with: '**`PositionHoldersSection` is changed in this PR** (round 3, after the claim's re-statement `6220867274`), with the same refusal state as `AssignedUsersSection`. `AssignedUsersSection`'s Add path still reads its refusal through the old key chain (`err.body.error`, `err.error`, `err.message`). That is an observation, not a defect measured here: the ObjectStack client sets `message`, which the chain's third arm reads.'"
          ],
          "recommendation": "Apply E1 to E6 together, in one body write by the seat. The body's only statement about PositionHoldersSection says it is untouched, which is false at 64dfb2e, and E6 also carries the Setup subtree's A/B/C record that the scope note asks for. None of the texts carries a closing keyword next to an issue number, and none has an angle-bracket shape."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: AssignedUsersSection's Add path still reads its refusal with the old key chain (err.body.error, err.error, err.message). For the ObjectStack client the third arm carries the server's sentence, so no wrong screen is measured. It is outside this round's single item, and is recorded in the proposed Acceptance-notes edit E6 only."
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #12739 comment 6221288675 · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-16T17:43Z. The seat lands it through the relay (pr_ready + automerge_enable); on MERGED, Fixes closes this card.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-16T17:59Z

    • PR fix(app-shell): Setup's root and metadata-admin routes wait on the app's access verdict; the holders section says a refused read (objectui#12706) #12739 merged through the merge queue as a88c206 on main (2026-10-16T17:57Z). Head 64dfb2e: ACCEPT 6221288675.
    • Read by content on main at a88c206:
      • packages/app-shell/src/console/AppContent.tsx carries appSurfaceAwaitsVerdict (Setup's root and the metadata-admin routes) and draws renderAppAccessDenied() on a denied verdict;
      • PositionHoldersSection.tsx and AssignedUsersSection.tsx each carry their *-read-failed state, which reads the server's sentence through extractWriteErrorMessage.
    • This card closed completed on the PR's Fixes. The seat strips pm:dispatched in the same stroke as this note.
    • Left open, not filed: the rest of the Setup subtree for a refused caller (shapes A / B / C in the PR's acceptance notes, per the seat's note 6220857048). The package-id spelling of the metadata routes rides objectui#12285.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions