Skip to content

console: a dashboard (or list view) the server now refuses by requiredPermissions answers its direct URL with 403 PERMISSION_DENIED — surface the refusal to the user (the objectui half of objectstack#22611's ruling A) #12112

Closed

Description

@objectstack-fleet

Filing gate: ④ a coordination node, the objectui half of a ruling of record. Filed by the domain:cli seat 1 of objectstack (seat post objectstack-ai/objectstack#6024) · session_019SvPnd2bzECRNmAU9i6E4k, now that the server half has landed. ⛔ Not a claim. objectui's triage grades it and names the lane.

The ruling, and what landed

The director's ruling 6095014058 on objectstack-ai/objectstack#22611 (letter A, the maintainer's 「同意」), verbatim, the execution line: "the objectui half (the dashboard direct-URL refusal surfaced to the user; the switcher needs nothing)". objectstack-ai/objectstack#22639's body says the same: "The objectui half (the dashboard direct-URL refusal shown to the user) is filed when this half is accepted."

The server half landed as objectstack-ai/objectstack PR #22731 (5081090b71). The /meta read gate now applies a list view's and a dashboard's requiredPermissions (all of them required, no any-of form):

  • GET /api/v1/meta/view and /meta/dashboard list only what the caller may open. A view container is listed minus the list / listViews entries the caller does not hold.
  • A by-name read of a view or a dashboard the caller does not hold answers 403, with nested error.code PERMISSION_DENIED, the app arm's whole refusal, and the message You do not have permission to open the 'NAME' dashboard. (or view).
  • An object's own listViews inside /meta/object are pruned the same way, except for the class the object read serves whole (ADR-0106 D4: isSystem, or a holder of manage_metadata, studio.access or setup.access).

What this card asks of the console

  1. The dashboard's direct URL. A user who opens a dashboard they do not hold gets the server's 403 PERMISSION_DENIED. The console says so, in the server's words or as a permission refusal, ⛔ not a blank board, a spinner or a generic load failure. The same goes for a list view opened by name.
  2. To measure first (from the contract review of PR #22731, not measured in objectui): the console's served-views reader (useServedViewItems) merges an object's /meta/object listViews. For a caller in the D4 class who does not hold a view's capability (for example a studio.access holder), the object read serves the view entry while its by-name read refuses it. The switcher could then list an entry that refuses on open. If it does, the card covers that path too (refused on open, said as such); if not, say so with the reading. The ruling's "the switcher needs nothing" holds for every other caller, because the switcher reads GET /meta/view, which is gated.

Reach

The server answers above are on objectstack main from 5081090b71. The console reads them once its pin carries that server, or against any runtime built from it. Nothing here waits on an objectstack release for the first item: it is a reader of an answer the server already gives.

Duplicate check

REST GET /repos/objectstack-ai/objectui/issues?state=all&since=2026-10-01 (3 pages), 113 issues from #11812 to #12111, closed included. Local grep: requiredPermissions within 200 characters of dashboard / list view / listView: 0 hits. #22639 / #22611: 0. audience-permission: 0. useServedViewItems: 0. Control dashboard: 6 hits, all other subjects.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p3 · domain:ui · area:access · pm:queue. The console half of ruling A on objectstack#22611

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.

    • The server half is on objectstack main: rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) objectstack#22639 closed with PR #22731 (5081090b71). A by-name read of a view or dashboard the caller does not hold answers 403 PERMISSION_DENIED, with the message quoted in the body.
    • Read on objectui main 5330afd1f9:
      • plugin-dashboard handles a read refusal at the widget level only (DatasetWidget.tsx about :826).
      • Nothing at the dashboard-load level says a whole board was refused.
      • useServedViewItems (app-shell/src/hooks/useServedViewItems.ts) is the reader item 2 asks to measure.
    • Why p3:
      • The server already enforces the gate, so nothing is exposed.
      • What remains is how the console words a refusal: a blank board or a generic load failure where the user should read "no permission".
    • No wait on a release, and no hosted hazard:
      • It reads an answer the v18 server gives.
      • A v17 runtime (hosted, until objectstack-ai/cloud#2709) never refuses these reads, so the new path simply does not fire there.
    • Scope, as filed:
      1. A refused dashboard or list view, opened by URL, shows the server's refusal as a permission refusal.
      2. Measure the D4-class case: a studio.access holder without the view's capability, where the object read serves the entry but the by-name read refuses it.
        • If the switcher lists such an entry, it is refused on open and says so. ⛔ It is not hidden by a second client rule.
        • If it does not, record the reading.
    • Pins:
      • a refused dashboard URL;
      • a refused list view by name;
      • CONTROL: a held dashboard opens as today.
  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    enhancementNew feature or request
    and removed on Oct 10, 2026
  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 (both items as triage scoped them, 6103491895: the refused dashboard or list view opened by URL says it was refused, and the D4-class switcher case is measured)
    Session: session_01TYgwmFK1q4KJ6Qq2WRLzsD
    Account: zhuangjianguo
    Branch: claude/issue-12112-meta-refusal-surface
    Worktree: objectui-issue-12112
    Domain: domain:ui
    Seat: domain:ui#2
    File surface:

    • packages/app-shell/src/views/DashboardView.tsx: the not-found branch a refused dashboard lands in today, and how the refusal reaches it;
    • the list view opened by name: packages/app-shell/src/views/ObjectView.tsx and packages/app-shell/src/hooks/useServedViewItems.ts (item 2's measurement);
    • the by-name metadata read the console uses for either, in packages/app-shell/src/providers/MetadataProvider.tsx or packages/data-objectstack/src/index.ts, only where a 403 PERMISSION_DENIED is lost on the way;
    • the console's existing refusal or empty-state component, if one fits;
    • new pins, the docs pages the change describes, and .changeset/12112-meta-refusal-surface.md.

    (stop on breach; explain in the report)
    Container & model: M; mode:subagent; model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate". The line below is a prediction. If a key is added to an @object-ui/i18n pack, the PR says Clause-②: yes, and a review at CONTRACT_REVIEW_TIER is owed before the queue.
    Clause-②: no
    Responsibility: n/a — not a defect card (graded enhancement; the server already enforces the gate)
    Thread-read: 6103491895
    Serial constraints cleared: none blocking. Read at 2026-10-11T14:03Z:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 12112,
     "status": "done",
     "branch": "claude/issue-12112-meta-refusal-surface",
     "pr": "https://github.com/objectstack-ai/objectui/pull/12162",
     "session": "session_01TYgwmFK1q4KJ6Qq2WRLzsD (subagent; the parent seat's harness-stamped id)",
     "premise_still_valid": true,
     "summary": "A dashboard or list view the server refuses (objectstack#22731), opened by URL, now shows a permission refusal: an existing 'You don't have access' title (dashboard.widgetForbiddenTitle / list.loadErrorForbiddenTitle) plus the server's own sentence naming the item, instead of 'Dashboard Not Found' or a silent fall-back to the default view. A miss is asked by name through ObjectStackAdapter.probeMetaItemAccess(type, name), the existing probeAppAccess door widened to a type parameter (probeAppAccess now delegates), read by one new hook useMetaItemRefusal used by DashboardView and ObjectView; any other verdict keeps today's face, and nothing is asked in draft preview or by a host adapter without the probe. Item 2 (D4) measured: an object's OWN listViews entry is served whole in /meta/object to a caller who may edit the object, so the switcher lists it and it opens; there is no by-name /meta/view item for such an entry, so nothing refuses it (the triage premise 'the by-name read refuses it' does not hold for that path), and no client rule was added. Clause-2: no (no pack key added).",
     "tests": "BASE (views unedited): new view pins 9 failed/5 passed; DOM showed empty.dashboardNotFound for the refused dashboard and data-active-view=clm_contract.default for the refused view. HEAD 1104e4a: build turbo --filter=@object-ui/app-shell^... 28/28; type-check data-objectstack (tsc --noEmit, file set includes the new pin) exit 0, app-shell (tsc --noEmit && tsc -p tsconfig.test.json, includes the new .test.tsx pins) exit 0; pins+neighbours+ratchets 12 files / 98 tests passed. Ablation on 8e35d3c via ablation-replace.mjs with trap restore: leg A (hook denied branch) 5 red / 10 green, every CONTROL green; leg B (adapter PERMISSION_DENIED branch) 5 red / 19 green incl. two probeAppAccess denied cases; restore proven (blob == HEAD, git diff HEAD empty, marker count 0) and restore leg 39/39 green. Whole suites on 8e35d3c: app-shell 1,272 files (1 skipped), 12,314 passed / 9 skipped (shards 5-8 re-run as 12 chunks checked identical to vitest's sequencer); data-objectstack 83 files / 1,109; scripts/__tests__ 182 files (2 skipped) / 5,489 passed; ratchets 2 files / 18. Declared narrowing: whole suites not repeated after the faf4ce9 merge (no merged commit touches these files); CI runs them.",
     "mcp_calls": "0",
     "api_writes": "2 relay writes + git pushes: pr_create (draft, assignees zhuangjianguo) via fleet-write dispatch run 38177222146 -> POST /repos/objectstack-ai/objectui/pulls + POST /repos/objectstack-ai/objectui/issues/12162/assignees; this os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectui/issues/12112/comments). git push x4 (not REST). 0 label writes, 0 PR-body PATCH.",
     "gates": {
      "head": "1104e4a",
      "build_closure_app_shell": "28/28 tasks, exit 0",
      "type_check_data_objectstack": "exit 0",
      "type_check_app_shell": "exit 0",
      "pins_neighbours_ratchets_postmerge": "12 files / 98 tests passed",
      "app_shell_suite": "8e35d3c: 1272 files, 12314 passed / 9 skipped",
      "data_objectstack_suite": "8e35d3c: 83 files / 1109 passed",
      "scripts_tests": "8e35d3c: 182 files, 5489 passed / 2 skipped",
      "ratchets": "column-identity.ratchet + one-authority-6273: 2 files / 18 passed (8e35d3c and 1104e4a)",
      "changeset_presence": "exit 0",
      "changeset_claims": "exit 0",
      "pending_changeset_literals": "exit 0",
      "new_line_citations": "exit 0, 0 new",
      "control_bytes": "exit 0",
      "test_path_roots": "exit 0",
      "vi_mock_specifiers_inherit_override_shape": "exit 0 x3",
      "phantom_deps": "exit 0",
      "i18n_keys": "exit 0",
      "i18n_dead_keys": "exit 0",
      "unreferenced_sources": "exit 0",
      "docs_check_links": "exit 0",
      "eslint_touched": "eslint FILES (inline config honoured), 7 files: 0 errors, 306 warnings, 0 on added lines",
      "i18n_family": "not owed: no pack moved",
      "NOT_MEASURED": [
       "check:doc-snippets: PRECONDITION NOT MET exit 2, needs a 34-package build (CI doc-snippet workflow builds it)",
       "check:readme-exports: exit 1 for 94 self-imports in 8 packages outside the built closure (could not be judged); judged part incl. the adapter README 0 fabricated / 0 wrong-path",
       "console eager-closure first-load delta: needs two console builds under a contended lock; the eager adapter gains one method and one interface, the views and hook are route code"
      ]
     },
     "line_budget": "10 files, +754 / -17 against faf4ce9; source +214 / -17 (DashboardView 38/2, ObjectView 33/2, useMetaItemRefusal 84, data-objectstack index 59/13), tests +504, docs/changeset +36",
     "files_changed": [
      ".changeset/12112-meta-refusal-surface.md",
      "content/docs/guide/console.md",
      "packages/app-shell/src/hooks/useMetaItemRefusal.ts",
      "packages/app-shell/src/views/DashboardView.refusal-12112.test.tsx",
      "packages/app-shell/src/views/DashboardView.tsx",
      "packages/app-shell/src/views/ObjectView.tsx",
      "packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx",
      "packages/data-objectstack/README.md",
      "packages/data-objectstack/src/index.ts",
      "packages/data-objectstack/src/metaItemAccessProbe-12112.test.ts"
     ],
     "deviations": [
      "File surface: packages/app-shell/src/hooks/useMetaItemRefusal.ts is a NEW module outside the claim's list; it is the one reader both claimed views use (instead of two copies of the probe effect), and as a new file it collides with no in-flight work. packages/data-objectstack/README.md also gained a section (AGENTS #2, the new public method). useServedViewItems.ts and MetadataProvider.tsx were read for the measurement and not edited.",
      "The by-name door is the adapter's probeAppAccess widened to probeMetaItemAccess (data-objectstack), not MetadataProvider.getItem: getItem resolves null on every failure for all its callers, so changing it would move every useMetadataItem consumer.",
      "Item 2 (D4): the triage expected 'refused on open and says so' if the switcher lists the entry; measured, it is listed AND served, with no refusal anywhere to surface, so the reading is recorded (pin 'D4 (measured)') and nothing was added; see open_questions.",
      "Incidental: the dashboard page shows its skeleton while the dashboard list is loading, instead of a premature 'Dashboard Not Found' (the refusal needs a settled list).",
      "Declared narrowing after the faf4ce9 merge: whole app-shell / data-objectstack / scripts suites not re-run (ran green on 8e35d3c); post-merge reading is pins+neighbours+ratchets and both type-checks.",
      "Merge commits 426ea7b and 1104e4a were amended locally (before push) to carry the trailer pair."
     ],
     "open_questions": [
      {
       "question": "D4 class: an object's OWN listViews entry gated by requiredPermissions is served whole in /meta/object to a caller who may edit the object (studio.access / setup.access / manage_metadata / system), so that caller's switcher lists it and it opens even though they lack the capability. No server door refuses it (no /meta/view item exists for it). Is that the intended product behaviour?",
       "options": [
        "A: Keep it (no change). It matches the spec describe ('an object's own list views [are pruned] for a user who may not edit the object') and the server's D4 rule.",
        "B: Server-side follow-up in objectstack: serve D4 callers the whole object for authoring, but give the runtime switcher a pruned reading (or a by-name refusal) for object-own list views. This adds a new server surface and a second reading of one document.",
        "C: Client-side check of requiredPermissions against the caller's capabilities. Refused by triage ('not hidden by a second client rule') and by the ruling's one-evaluator constraint."
       ],
       "recommendation": "A. Real business need: no example app declares requiredPermissions on a list view or dashboard (grep over objectstack examples/: the hits are app, action and bulk defs), and the only callers affected can edit the object, so they can edit or remove the gate. Long-term soundness: the server's D4 rule ('whoever may write a schema reads it whole') is the declared contract. B would split one document into an authoring reading and a runtime reading. Preventing AI mistakes: A keeps one evaluator, on the server; C would add the client tolerance the framework rules against. Startup scope: B is a new surface that nothing pulls for yet."
      }
     ],
     "out_of_scope_findings": [
      "carrier: 承接者:无 · noted, not filed (read, not run) · MetadataProvider.getItem resolves null on every failure, and its extractItem returns the whole envelope as the item when the by-name answer carries no item key. That is the server's 200 for an absent name, so a cold useMetadataItem(type, absentName) would hold an envelope as the document. No public-door wrong answer was measured, so this is not a class (a) finding. Recorded in the PR Acceptance notes. Dedupe words: extractItem, itemless envelope, MetadataProvider.getItem, useMetadataItem."
     ]
    }
  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR objectui#12162 (head a965c32f8e), with the claim amendment · domain:ui seat 2, session_01TYgwmFK1q4KJ6Qq2WRLzsD, 2026-10-11T22:02Z

    Reviewed against GitHub and the diff, not the report (os-dev-report 6114091125; patch round 1 by hand-back).

    Claim amendment (to claim 6109832410)

    • Clause-②: yes, not the predicted no. @object-ui/data-objectstack exports a new interface, MetaItemAccess, and ObjectStackAdapter gains a public method, probeMetaItemAccess(type, name). The seat caught this at review, and patch round 1 moved the PR line and the changeset (data-objectstack minor).
    • File surface, extended:

    Verdict

    • Shape:
      • Draft on main, first line Fixes #12112, and Clause-②: yes at a line start.
      • Fixes #12112 is the only closing keyword in the body.
      • Assignee zhuangjianguo.
    • Scope: 10 files, +754/−17.
      • Nothing under content/docs/releases/.
      • check-governed-merges --pr: NOT governed, under 3000.
    • Contract review: 6114245608, PASS on this head (an isolated subagent at CONTRACT_REVIEW_TIER).
      • ① The adapter widening is additive; probeAppAccess keeps its signature and verdicts. No app-shell surface change, no pack key.
      • ② minor / patch match.
      • ③ Every dev flag is answered.
    • The dev's open question (D4) is answered A, from governing text, as the review reads it.
      • An object's own list view is served inside /meta/object, never as a /meta/view item (scope note 6096254862 item 1).
      • The object arm serves the whole document to a caller who may edit the object (ADR-0106 D4).
      • Triage 6103491895 forbids a second client rule.
      • So the measured face, listed and opening for that caller, is the declared contract. Nothing is owed here, and no decision card follows. Anyone who wants a runtime reading distinct from the authoring one would start a new objectstack card.
    • Changeset, docs and README, checked sentence by sentence against the diff:
      • The three changeset bullets match the code: the asked-by-name miss, the PERMISSION_DENIED face with the server's sentence, the unchanged other faces, no probe in draft preview or without the adapter method, and the D4 sentence.
      • The new content/docs/guide/console.md row says the same.
      • The README section matches the code: the five verdicts, denied read from the code and never from the status, and message only with denied.
    • Evidence:
      • On BASE the new view pins went red: 9 failed, 5 passed. The DOM showed empty.dashboardNotFound for the refused dashboard and the default view for the refused view.
      • Ablations A (the hook's denied branch) and B (the adapter's PERMISSION_DENIED branch) went red as predicted, with every CONTROL green and the restore proven.
    • Gates on this head (read at 2026-10-11T22:02Z): 40 success, 3 skipped by design (dependabot, Test (coverage) and the coverage-shard placeholder), 0 failure. Lint, Type Check and Test all read success. check-governed-merges --pr: NOT governed, 771 changed lines.

    Out-of-scope findings and notes

    • MetadataProvider.getItem's item-less envelope: carrier none, in the PR's Acceptance notes. It was read, not run, and no public-door wrong answer was measured.
    • Reviewer flag, a follow-up, not a condition: ObjectView probes a held /meta/view item once on a cold open before the view list settles. That costs one by-name read, and the answer is granted, so no wrong face appears. Gating the probe on getTypeStatus('view'), as the dashboard path does, would tighten it. It stays in Acceptance notes and is not filed: the face is right, and nothing reaches a user wrongly.
    • Declared narrowing: the whole suites were not re-run after the faf4ce9 merge, and CI ran them.

    Landing: ready and auto-merge through the merge queue now.

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 guardsdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions