Filed by the triage seat implementing a maintainer ruling (2026-08-12, live PM session, verbatim: 「接受你的全部建议。」 on the decision digest covering objectui#4252). Contract-first split: this is the backend half; the console half is objectui#4252, which is blocked on this card.
Problem (measured in objectui#4252)
GET /api/v1/meta/apps omits apps the session isn't authorized for (filterAppForUser, packages/rest/src/rest-server.ts). From the console's point of view an unauthorized app and a nonexistent/unpublished app are byte-identical — both absent — so the console renders "may still be publishing" for a permanent authorization denial. That copy already cost two acceptance-test batches chasing a "platform defect" that was a missing permission-set binding (evidence on objectui#4252).
Ruled scope
- By-name app route: when the app exists but the session lacks its
requiredPermissions, answer an explicit permission-denied envelope (code + status, standard error shape) instead of the absence/404-equivalent. - List route (
/meta/apps) stays filtered as-is — deliberately unchanged, to avoid widening the enumeration surface beyond what a direct by-name probe already implies. - The console (objectui#4252) then distinguishes: absent ⇒ current "not published" copy; denied envelope ⇒ plain "you don't have access to this app".
Acceptance
- Session WITHOUT the capability, by-name request ⇒ permission-denied envelope (assert
code + status, not just non-200). - Session WITH the capability ⇒ app unchanged.
- Nonexistent app name ⇒ existing absence behavior unchanged.
- List route output for the unauthorized session unchanged (no
authorized: false leakage).
Landing
packages/rest/src/rest-server.ts (filterAppForUser and the by-name meta app handler) ⇒ domain:cli. If implementation shows the by-name path actually resolves in metadata-protocol instead, report the corrected landing in the claim — do not silently widen the file surface.
Links
Filed by the triage seat implementing a maintainer ruling (2026-08-12, live PM session, verbatim: 「接受你的全部建议。」 on the decision digest covering objectui#4252). Contract-first split: this is the backend half; the console half is objectui#4252, which is blocked on this card.
Problem (measured in objectui#4252)
GET /api/v1/meta/appsomits apps the session isn't authorized for (filterAppForUser,packages/rest/src/rest-server.ts). From the console's point of view an unauthorized app and a nonexistent/unpublished app are byte-identical — both absent — so the console renders "may still be publishing" for a permanent authorization denial. That copy already cost two acceptance-test batches chasing a "platform defect" that was a missing permission-set binding (evidence on objectui#4252).Ruled scope
requiredPermissions, answer an explicit permission-denied envelope (code+status, standard error shape) instead of the absence/404-equivalent./meta/apps) stays filtered as-is — deliberately unchanged, to avoid widening the enumeration surface beyond what a direct by-name probe already implies.Acceptance
code+status, not just non-200).authorized: falseleakage).Landing
packages/rest/src/rest-server.ts(filterAppForUserand the by-name meta app handler) ⇒domain:cli. If implementation shows the by-name path actually resolves inmetadata-protocolinstead, report the corrected landing in the claim — do not silently widen the file surface.Links
enable.apiEnabledis never consulted byfilterAppForUser#7912 (filterAppForUsernever consultsenable.apiEnabled— separate card, do not fold in)