Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed as not planned

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/console depends 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:

export const STUDIO_ENTRY_CAPABILITY = 'studio.access';
export function holdsStudioAccess(systemPermissions: string[] | undefined): boolean {
  if (!Array.isArray(systemPermissions)) return false;
  return systemPermissions.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 both apps/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

Activity

  1. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    PM · 升级到决策箱 —— 这是分层决策,不是席位裁量

    Raised by the #5521 dev, which hit this as a hard blocker: it shipped the features.aiStudio half of #5521 and could not ship the studio.access half, because holdsStudioAccess lives in apps/console, which depends on @object-ui/app-shell. The import is impossible in the required direction. That is a structural fact, not a preference.

    Escalating rather than ruling: one option adds a new export to a published package (clause ② territory, and Fable 5 is exhausted for this seat, so it would wait regardless), and the other is a scoping decision about what a library surface may know. Both are architecture shape.

    Option C — re-derive the predicate inside app-shell — is ruled out already, and not by me: PR #5563 argued against exactly this in its own words, "one definition of 'is this principal a platform operator' … so the two surfaces cannot drift into two spellings of one fact." Minting a second spelling here would undo a convergence this lane landed today.


    四棱分析(业务角度)

    一句话问题: 平台的界面库(嵌在控制台里,也嵌在别的宿主里)现在没法知道「这个人是不是平台运维」—— 那个判断住在控制台里,而控制台是建在界面库之上的,底层够不到上层。要把这个判断挪到公共层,还是干脆规定界面库不问这种问题?

    选项(客户可感知的代价):

    做法 客户看到什么
    A 把判断提到公共权限包,两边共用一份 全平台「平台运维」只有一个定义,不会漂;但公共包多一个对外 API,永久维护
    B 规定界面库只问「这个部署有没有这个能力」,「这个人有没有权限」留给控制台的路由 分层干净、不加对外面;残余是:某些部署下卡片仍会显示,点进去在路由被挡回来
    C 界面库自己再写一份判断 ⛔ 已排除 —— 正是本车道今天刚收敛掉的「同一事实两种拼法」

    业务直译与行业类比: 相当于门禁读卡器(通用硬件)要不要认得「谁是维修工」。A 是把「维修工名册」放进所有楼都能读的公共库;B 是规定读卡器只管「这扇门今天开不开」,「你是不是维修工」由楼里的前台判断 —— 代价是维修工可能先走到门口才被前台拦下。

    四轴论证:

    推荐: 倾向 B,但这是需要你拍板的分层决策,我不代裁。理由:app-shell 被控制台以外的宿主嵌入,而 studio.access 目前是控制台的路由策略;把库的首页耦合到它,是设计决定而非机械编辑。若平台希望在原则上拥有一份共享的「运维」判据,那 A 才是对的答案 —— 那时它应作为一次公开面扩展被正式裁决,而不是作为某张卡的顺手活。

    本分析看不见什么: 我没有度量 app-shell 究竟被控制台以外的哪些宿主嵌入、它们各自是否有等价的主体级门 —— 若实际上只有控制台一个消费者,B 的「分层洁净」收益大幅缩水,A 的成本也随之显得可接受。另外我没有核算把判据提进 @object-ui/permissions 的实际改动面(#5576 提到可从 studioEntry.ts 再导出以保住既有 importer 与 docblock,但我没有验证那条路径的完整代价)。


    Labels: needs-user-decision + domain:ui. Parent card #5521's shipped half is unaffected either way — it lands on the deployment flag alone.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage (decision-inbox intake): the PM analysis above is complete in substance but lacks the machine-findable marker, so extraction sweeps cannot see it. Backfilling the standard block below, distilled from that analysis — it adds nothing new; the full reasoning stays in the comment above. — triage seat, session session_016N5bHZr1DG4g9RK7v36YtV

    ① 项目长远合理性:A(判据上提公共权限包)= 全平台「平台运维」一个定义,与 #5563 收敛同向;B(库只问部署级,主体级留路由)= 分层规则更清晰、不加公开面;C(库内重写一份)已被 #5563 先例排除。
    ② 实际业务拉动:B 的残余已实测限定为一次可恢复回弹(卡片显示→路由挡回),损伤低于 #5521 原度量的「填完表单才被拒」;今天没有库外宿主在拉动 A。
    ③ 防 AI 犯错:A 机制上更强(一个定义,无两拼法可猜);B 依赖分层纪律,纪律可被下一人破坏。
    ④ 创业阶段不扩散:A 给公共包新增永久维护的导出;B 零新公开面 —— 此轴明确指向 B。
    推荐:B(倾向,不代裁 —— 分层决策属维护者);若平台原则上要一份共享「运维」判据,则 A 应作为公开面扩展被正式裁决。
    本分析看不见什么:app-shell 除控制台外的真实宿主清单未测(若仅控制台一个消费者,B 的收益大缩、A 的成本可接受);A 路线经 studioEntry.ts 再导出的完整改动面未核算。
    裁决格式:回一个字母即可;「B」= 本卡关闭 answered、#5521 的 studio.access 行记「按 B 答复不实现」;「A」= 换 pm:queue 派 ui 席(条款② yes,公开面扩展)。


    Generated by Claude Code

  4. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Marker repair (triage seat, same session): the facets-block marker in my backfill comment above was written raw and the comment sanitizer ate the line — the block content survived. Machine-findable marker in sanitizer-safe spelling: <!-- os-decision-facets --> — the block is the ①–④ + 推荐 + 置信缺口 sequence in the backfill comment above.


    Generated by Claude Code

  5. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling recorded (2026-08-22, decision-inbox digest with the skills seat, session_01ApyDuQY2fkunMCqXiqvBhR)

    The full decision inbox (46 cards) was presented with per-card four-axis recommendations; the maintainer accepted the batch, verbatim: 「接受所有」.

    Ruled: Option B — the layering rule: the library (@object-ui/app-shell) asks only the deployment-level question (features.aiStudio); the principal-level question ("does this person hold studio.access") stays with the console's route gate. No new export is added to @object-ui/permissions, and no predicate is re-derived in app-shell (Option C stays excluded per #5563). Accepted residual, as measured: under aiStudio: true with a fail-open permissions fetch, the card can still show and the click is bounced at StudioRoute — a recoverable rebound. Per the card's own ruling format, "B" means this card resolves as answered rather than implemented, and #5521's studio.access row is recorded as answered per B, not implemented.

    State: needs-user-decision removed → pm:queue (ready for dispatch).


    Generated by Claude Code

  6. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    State correction by the skills seat (session_01ApyDuQY2fkunMCqXiqvBhR): the ruled Option B is terminal for this card — per the card's own wording, it "closes as not planned and #5521's studio.access row is answered rather than implemented". No dispatch work remains, so the interim pm:queue is removed and the card is closed as not planned. The answered-not-implemented record has been posted on #5521.


    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

    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