Skip to content

security/explain needs a batch form (recordIds[]) — the list row kebab cannot consume the record-level verdict without 2N probes per page #8326

Description

@yinlianghui

Filed from objectui#4296's measured escalation (objectui whole-repo seat, session_017Qqyix2QcnpUC9XeYVDzx3). This is the framework half of closing that card; the objectui half is blocked on it.

The consumer problem (measured in objectui): the list row kebab offers Edit/Delete on records the server will 403 (object-level grant present, record-level verdict — writeScope/sharing/RLS — negative). The detail surface answers this correctly via useRecordEditable, which POSTs /api/v1/security/explain with {object, operation, recordId} and relays decision.record.visible. That design is deliberate (ADR-0090 D6 / ADR-0095 C2: the record-level verdict is server-authored so buttons and enforcement middleware cannot diverge) and we do not want a client-side reimplementation — a dial-based predicate (modifyAllRecords + owner comparison) measurably over-hides on sharing-rule grants: a record shared read-write to a non-owner under modifyAllRecords:false is writable per the server and would be hidden by the client predicate. Capability regression, rejected.

The gap: rest-server.ts's explain endpoint (validated by spec/src/security/explain.zod.ts) accepts a SINGULAR optional recordId. A list page that wants the same verdict per row needs 2 probes per row (update+delete) — a 50-row page is 100 POSTs. plugin-detail's own suite comments already record the per-mount multiplier (2 probes per DetailView mount).

The ask: a batch form of the same evaluation — recordIds: string[] on the existing request shape (or a sibling batch endpoint if the spec prefers), answering the per-record decision.record.visible map for one (object, operation) pair in one round trip, same layering semantics as the singular form, documented ordering/missing-record behavior. With that, the objectui side folds the verdict synchronously at page level into the grid's existing per-row predicate channel, and the kebab/detail truth table becomes a literal agreement test.

Sizing note: the singular path's evaluation pipeline presumably loops per record anyway; the batch form is a transport amortization, not a new semantic. If the spec side prefers a cap (e.g. max 200 recordIds), the consumer paginates under it happily.

Refs: objectui#4296 (the consumer card, now Blocked-by this one), ADR-0090, ADR-0095. Filed unassigned for the framework seat's triage.


Generated by Claude Code

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions