Skip to content

finding: an approver routed a request can be unable to open the record it concerns — the inbox's record link dead-ends on "Record not found" #7345

Description

@os-zhuang

Observation-class finding recorded during the #7331 browser verification run. Filed unassigned, no pm:queue — for triage to grade. It is not a routing regression from #7213; the routing half was verified correct and is stated below so the next reader does not re-derive it.

What happens

A non-admin who is a pending approver opens the Approvals Inbox, sees the request, and clicks the record chip in its row (INV-1002). The record page renders "Record not found — The record you are looking for does not exist or may have been deleted."

The record exists and the request is live.

The routing is correct — that part was ruled out first

The rendered href is exactly what #7213 intended: it carries the current app's segment, the right object and the right record id.

INV-1002 -> /_console/apps/com.objectstack.account/showcase_invoice/record/9oAya50sd_E-PEWH

Loading the same URL shape under the same app with a record the persona can read renders the record page normally (verified with showcase_project/record/1YtAy4jz7Wsi9m9k under com.objectstack.account). So the account app can host a record page for a foreign business object; nothing about the app segment is wrong.

The real cause: record-level visibility

The authoritative server answer for that persona is a 404, not a 403:

GET /api/v1/data/showcase_invoice/9oAya50sd_E-PEWH
404 {"error":"Record 9oAya50sd_E-PEWH not found in showcase_invoice","code":"RECORD_NOT_FOUND"}

The persona holds contributor, whose showcase_invoice grant is owner-scoped; the invoice is owned by another user, so it is filtered out of the row set before the by-id read. The same read as admin returns the record. Transferring owner_id to the persona did not change the outcome either (the seeded showcase rows carry organization_id: null), so the showcase fixture makes this easy to hit — but the general shape is not showcase-specific: approver resolution routes on positions, record visibility is a separate gate, and nothing reconciles the two.

Why it is worth recording rather than shrugging at

The drawer degrades well: it carries the request's payload snapshot (record title, object, subtotal, account, status), so an approver can decide without opening the record, and the decision itself works — the run approved this exact request from the drawer successfully. So this is not a blocked approval.

What is unsatisfying is the affordance: the inbox offers a record link that, for the person it is offered to, leads to a page saying the record may have been deleted. The message is also the least useful of the available truths — the honest one is "you do not have access to this record", which the platform renders elsewhere (sys_inbox_message renders exactly that for the same persona).

Candidate dispositions (not decided here)

  1. Leave it. Approvals decide from the snapshot by design; record access is the security model's business. Cheapest, and defensible.
  2. Suppress the record link in an inbox row when the viewer cannot read the target — needs a per-row readability probe the inbox does not do today.
  3. Keep the link and fix only the landing message: distinguish "not visible to you" from "does not exist" on the record page. Smallest user-facing improvement, no security change.
  4. Grant an approver implicit read on the record under approval for the life of the request. The largest surface by far — it makes approver routing a source of record visibility, which is a real security decision and should not be taken as a UX fix.

No option recommended; 4 in particular is a security-model change, not a polish item.

Evidence

Measured 2026-08-10, examples/app-showcase, framework 88154bee1, console dist stamp 8aad9fd50b16750aebdd294392ff79540f17cd32. Non-admin persona holding contributor + finance. Screenshots of the "Record not found" landing, of the same URL shape rendering a readable record, and the paired API reads were captured in the run.


Generated by Claude Code

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: escalated — finding removed, now needs-user-decision; routing stays deliberately open (the ruling decides the landing: objectui suppression or message, identity grant, or accept-as-is).

    • Why now: this seat's 09:20Z and 10:20Z round briefs allowed the card two rounds unrouted before escalation, and this round is that deadline. Leaving it finding-held keeps a real UX-vs-security fork invisible to every lane; the four dispositions are written and mutually exclusive — a decision card's exact shape.
    • The four options (from the card): 1. leave it (approvals decide from the drawer snapshot by design — the decision path itself works); 2. suppress the record link when the viewer cannot read the target (needs a per-row readability probe the inbox does not do today); 3. keep the link, fix only the landing message — distinguish "not visible to you" from "does not exist" (smallest user-facing improvement, no security change; the platform already renders the honest message elsewhere); 4. implicit approver read on the record for the life of the request (a real security-model change — approver routing would become a source of record visibility). Recommendation: option 3 — honest, small, forecloses nothing; option 4 must not ride in as a UX fix.
    • Premise: measured 2026-08-10 on the showcase run (framework 88154bee1, console pin 8aad9fd5); the mechanism — approver resolution routes on positions, record visibility is a separate gate, nothing reconciles the two — is structural, not fixture-specific. No staleness concern.
    • Dedup: The Account app's Notifications entry 403s for every non-admin — no permission set grants sys_inbox_message #7344 (missing sys_inbox_message grant, escalated this round) and [console] Four remaining inbox/activity entries still hardcode /apps/setup/..., so a business user without setup access has no working "all notifications" or "all activity" link #7266 (bell routing) are adjacent-but-distinct inbox-family cards; objectui#4110 (popover filter regression) likewise. Cross-linked. Clean.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 11, 2026

    @claude
    Contributor

    Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

    Ruling: two halves, split. (1) The short-term half is queued on THIS card: the approvals inbox renders an honest "record not visible to you" state on the record chip instead of dead-ending on "Record not found" — a UI-truthfulness fix, no security-model change. Lands in the console (routing label repo:objectui applied). (2) The model question — does approver routing imply record read visibility — is deliberately NOT ruled quickly; it is now #7497 (needs-user-decision, approvals), explicitly not v17.

    State: needs-user-decision → pm:queue + repo:objectui.

    Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Moved to objectstack-ai/objectui#5211 — closing here as moved, not as resolved. Undecided and unfixed.

    Provenance, three parts: whose instruction — the maintainer's; verbatim — 「转」, following 「objectstack 仓库中还有很多 objectui 任务为什么没关掉」; where — said directly to the repo:objectui PM seat in session session_01RV6yuVCxymHYE16PL9vQkE, 2026-08-18, under the standing direct-dispatch authorisation.

    Why moved: of the four candidate dispositions, the two that are genuinely polish — suppressing the record link when the viewer cannot read the target, and distinguishing "not visible to you" from "does not exist" on the landing page — both land in objectui. Disposition 1 is "leave it" and needs no repo. So every actionable branch except one lives in the destination repo.

    ⚠️ The exception, carried across deliberately: disposition 4 — granting an approver implicit read on the record under approval — is a security-model change, as the original body says plainly, not a UX fix. If that is ever the chosen direction it comes back to this repo as a contract-first split, and ⛔ must not be implemented in objectui as a polish item. That warning is reproduced in the moved card so it cannot be lost by someone reading only the destination.

    One label did not survive: the original carried approvals, which does not exist in the objectui repo. ⛔ I did not mint it — the association is recorded in prose on the moved card instead. Flagging because a label-based query for approvals work will no longer find this card.

    The 2 comments here stay readable and were not copied. Anyone tracking this: follow objectstack-ai/objectui#5211.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions