Skip to content

holdsStudioAccess is unreachable from @object-ui/app-shell, so the library's surfaces cannot share the console's one definition of "platform operator" #5576

Description

@os-sales

Found while implementing #5521 (PR #5575), where it blocked one of that card's two proposed signals. Deliberately left out of that PR's scope.

Claim

holdsStudioAccess(systemPermissions) — the fail-closed predicate for "may this principal enter Studio", and this repo's single definition of "is this principal a platform operator rather than a business user" — lives in the console app:

apps/console/src/components/studioEntry.ts

apps/consoledepends on@object-ui/app-shell. So no module in packages/app-shell/** can import it: the dependency runs the other way and app-shell's manifest declares no console dependency (it could not, without a cycle).

That is fine while every consumer is a console page — which was true when #5563 reused it for the Approvals raw-payload panel. It stops being true as soon as a surface inside the library needs the same answer.

Where it just bit

#5521 asks the Console home page to honour studio.access for the "Build an app" card. That page is packages/app-shell/src/console/home/HomePage.tsx, re-exported as DefaultHomePage — inside the library. PR #5575 closed that card's features.aiStudio half and had to leave the studio.access half undone, because the only ways to do it were:

  1. re-derive the predicate in app-shell — the exact drift Approvals Inbox: the raw payload panel is a platform-operator affordance, not the approver's read path #5563 argued against, in its own words "one definition of 'is this principal a platform operator rather than a business user', so the two surfaces cannot drift into two spellings of one fact";
  2. relocate it, which is this issue;
  3. skip it.

#5575 took (3) and recorded why.

Not claimed

That studioEntry.ts is in the wrong place for the route gate. useStudioEntry — the hook, its retry policy, its endpoint, its pending/failed states — is console routing policy and belongs where it is. Only the pure predicate plus its capability constant are the shared fact:

exportconstSTUDIO_ENTRY_CAPABILITY='studio.access';exportfunctionholdsStudioAccess(systemPermissions: string[]|undefined): boolean{if(!Array.isArray(systemPermissions))returnfalse;returnsystemPermissions.includes(STUDIO_ENTRY_CAPABILITY);}

It has no React dependency and no fetch dependency — it is a membership test over the array MePermissionsResponse already carries.

Shape of a fix

@object-ui/permissions is the natural home: it already owns MePermissionsResponse, already exposes the raw systemPermissions: string[] | undefined on its context, and is already a dependency of bothapps/console and packages/app-shell. Move the predicate and the constant there; re-export from studioEntry.ts so its existing importers and its docblock stay put.

The load-bearing part is the inversion doctrine, which must travel with the code rather than being left behind as a comment on a re-export: this predicate fails closed on undefined, opposite to usePermissions().hasCapabilities, which fails open by design. Both live in @object-ui/permissions after this move, one module apart, so the docblock explaining why they differ is more necessary there than it is today, not less. The undefined-vs-[] distinction objectui#4656 preserved is what makes the difference expressible at all.

Worth weighing before doing it

Whether a library surface should consume studio.access at all is a real design question and not settled by this issue. @object-ui/app-shell is embedded by hosts other than the console, and studio.access semantics are currently the console's route policy. A reviewer may reasonably conclude the home page should keep asking a deployment-level question (features.aiStudio, which is what #5575 shipped) and leave principal-level Studio entry to the route gate. In that case this issue closes as not planned and #5521's studio.access row is answered rather than implemented — which is a fine outcome, but it should be decided rather than defaulted into.

Refs


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions