Repository navigation
frontend(desktop): hide the desktop entry from users the socket will refuse, with i18n in all 11 locales #17370
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 24, 2026 Taking this. Two amendments first, one of which makes the second criterion produce the exact disagreement it forbids.
AC2 cannot be met with
hasPermission, because that function has the short-circuit #13854 removedThis issue says:
if the frontend gates on the same permission it will agree with the socket automatically, including on that [superadmin]
It will not.
autobot-frontend/src/composables/usePermissions.ts:88-92:const hasPermission = (permission: Permission | string): boolean => { // Admin always has all permissions if (userStore.isAdmin) return true return permissions.value.includes(permission as Permission) }
And
useUserStore.isAdminisisAdminRole(currentUser.role), which admits superadmin — its own test says so in as many words (useUserStore.isAdmin.test.ts:46: "admits superadmin — a hand-rolled === 'admin' check previously rejected this").So, from the generated table:
Role ROLE_PERMISSIONSholdsmcp.desktop.controlsocket (asks the table) hasPermission(...)adminyes (63 perms) admits true operatoryes (34 perms) admits true superadminno — 0 perms refuses true A superadmin would be shown the desktop entry and then closed with 1008 on the handshake. That is the disagreement AC2 exists to prevent, and gating on
hasPermissionis what would create it.This is #13854's ruling reappearing one layer up. PR #17369 refused to write
is_admin_role(role) or role_has_permission(...)in the backend gate precisely because #13854 removed the administrative short-circuit from the permission predicate.hasPermissionis that short-circuit, alive in the frontend composable. Gating on it would reinstate in the UI the thing the backend was careful not to reinstate.The codebase already knows this hazard, three lines away from the same store —
useUserStore.ts:107-109, forservice.management:superadmindeliberately excluded: itsROLE_PERMISSIONSentry is empty by …AC2 is amended to: the gate reads the role permission table — the same source
role_has_permissionreads — with no administrative override, so the frontend and the socket agree on every role includingsuperadmin.usePermissionsalready exposespermissions(table-derived, no short-circuit); I am adding a named, documentedholdsPermissionalongsidehasPermissionso the next call site that needs to match a backend gate has one to reach for instead of rediscovering this.hasPermissionis not being changed. It has 60-odd call sites whose expectations I have not audited, and changing an administrative override's semantics across the app is not this issue's risk envelope. That is worth its own issue and I will file it.AC4 understates itself: all five tab labels are hardcoded, not one
ChatTabs.vue:36-43— the wholewithDefaultsdefault:{ key: 'chat', label: 'Chat', icon: 'comments' }, { key: 'files', label: 'Files', icon: 'folder' }, { key: 'terminal', label: 'Terminal', icon: 'terminal' }, { key: 'browser', label: 'Browser', icon: 'globe' }, { key: 'novnc', label: 'noVNC', icon: 'desktop' }
The issue names
'noVNC'. The other four are the same violation in the same expression, and fixing one of five while editing the line would be the "patched again in every PR that finds it" shape. All five go to i18n.The two unassessed entry points, checked rather than assumed
Entry point Verdict views/AdvancedControlView.vuenot an entry point. Its only noVNC references are capability readouts — novnc_available ? t('common.yes') : t('common.no')at:201-206and:330-331. It reports whether the feature exists; it does not open it. No gate needed, and gating it would hide diagnostics from the people who need themviews/DesktopView.vueis an entry point — 411 bytes, mounts DesktopInterface.vue. But/desktopredirects to/chat(router/index.ts:905), so the view is unreachable by URL. Unreachable-but-mounted is unfinished work rather than litter, so it is in scope for the gate rather than ignoredThird entry point, reachable and needing the explanation rather than a hidden control:
/slm/tools/novnc→SlmNoVncView.vue(router/index.ts:908-915,requiresAuth: trueand nothing else). A bookmarked URL is activation, so hiding a tab does not cover it.- added a commit that references this issue
on Sep 28, 2026
#17054's fourth criterion, split out because it is a different risk envelope from the backendgate and more than one honest review pass combined with it. The backend half is PR
#17369.What is already true, so this is not re-derived
The mechanism exists.
usePermissions().hasPermission('mcp.desktop.control')works today:autobot-frontend/src/types/_generated/workflow.tscarriesROLE_PERMISSIONSauto-generatedfrom the backend table (
gen_frontend_types.py), and it already contains'mcp.desktop.control'. So the frontend can ask the same question the socket now asks, from thesame source, with no new plumbing and no drift risk.
What the socket now does (PR
#17369): a user credential must holdmcp.desktop.control—adminandoperatortoday. Everyone else is refused in the handshake with1008. A paireddevice is unaffected. Note
superadminis also refused, byROLE_PERMISSIONS[Role.SUPERADMIN]being empty (
#13854); if the frontend gates on the same permission it will agree with thesocket automatically, including on that.
Entry points found
novnctab —components/chat/ChatTabs.vue:42{ key: 'novnc', label: 'noVNC', icon: 'desktop' }in awithDefaultsprop default. The label is a hardcoded English string, which is its own violation of the no-hardcoded-UI-strings rule, so this line needs i18n regardless./slm/tools/novnc—router/index.tsmetacarriesrequiresAuth: trueand nothing else.views/AdvancedControlView.vue,views/DesktopView.vue/desktop/chat.The router has no role or permission mechanism today —
grep 'requiresRole\|requiresPermission\|roles:'overrouter/index.tsreturns nothing. So gating the route means introducing the first such meta and a guard that reads it, where gating the tab is a filter at the component. Those are different sizes; the choice belongs to whoever takes this.Acceptance criteria
mcp.desktop.controldoes not see, or cannot activate, the desktop entry atevery point listed above — with the unassessed two checked rather than assumed absent.
hasPermission('mcp.desktop.control'), the same permission the socketenforces, so the two cannot disagree. Not a role list, and not a second copy of the rule.
string in all 11 locales (
ar, de, en, es, fa, fr, he, lv, pl, pt, ur).ChatTabs.vue:42's hardcoded'noVNC'label moves to i18n as part of the same change.holding it does — the pair, since "hidden for everyone" would satisfy the first alone.
Explicitly not a security boundary
The socket is the boundary and it is closed by
#17369. This issue is about not offering acontrol that will be refused — a UX and honesty fix, not a second gate. It should never be the
thing standing between a user and the desktop, and a reviewer should push back on any
implementation that reads as though it is.
Related
#17054— parent; criteria 1-3 delivered in#17369#15146— the paired-device path, unaffected