a naked platform does not describe what it does not have

Reversing my own call from an hour ago. I built the denied-route screen to EXPLAIN the
absence — "Music is not installed", with a link to the app store — and argued a redirect
erases what you asked for. The owner's correction is the better principle: a server should
not know about a sidecar it does not have. Explaining Music is the app describing a feature
that, as far as this install is concerned, does not exist, and it leaks the whole catalogue
of what could be installed to any member who types a URL.

So a denied path is now indistinguishable from an unknown one: redirect home, the same
answer App.tsx's path="*" already gave. One behaviour for a member without a grant, an
owner without the sidecar, and a typo. Nothing disclosed.

The Permissions screen loses both explanatory blocks for the same reason. One listed every
capability whose sidecar is absent — a catalogue of uninstallable features presented as a
permissions decision. The other described chat, tasks, the desktop and the wallet as
"not grantable" to an owner who may have none of them installed. `notInstalled` is gone
from the API too, not just hidden in the UI. What is on that screen is what this server can
actually do.

Still short of what the owner described, and worth naming rather than implying otherwise:
routes are DECLARED in App.tsx for every screen and this hides the ones that should not
resolve. The end state is routes REGISTERED from the manifests of installed sidecars, so an
uninstalled feature has no route to hide. The manifests already exist and the dock is
already built from them; the router is not, yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:51:41 +00:00
co-authored by Claude Opus 5
parent b4f88ec161
commit eda004a46d
3 changed files with 33 additions and 89 deletions
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Loader2, Lock, PackageOpen } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { CAPABILITIES_QUERY_KEY } from 'hooks/useCapabilities';
import { Button } from '@/components/ui/button';
@@ -29,8 +29,7 @@ type Grant = { role: string; capability: string; level: 'read' | 'write' };
type CapabilitiesResponse = {
/** Grantable AND installed. What this server can currently do. */
capabilities: CapabilityInfo[];
/** Grantable, but no sidecar installed — listed so their absence reads as a fact, not a bug. */
notInstalled: CapabilityInfo[];
roles: string[];
grants: Grant[];
};
@@ -164,36 +163,12 @@ export const PermissionsSection = () => {
})}
</div>
{/* Absent because nothing is installed, not because they cannot be shared — a different sentence from
the one below, and the two used to be indistinguishable (both were simply missing). */}
{data.notInstalled.length > 0 && (
<div className="flex gap-3 rounded-lg border border-dashed p-3 text-xs text-muted-foreground">
<PackageOpen className="mt-0.5 h-4 w-4 shrink-0" />
<div>
<div className="font-medium text-foreground">Nothing installed for these yet</div>
{data.notInstalled.map((c) => c.label).join(', ')} each appears here once you install it from the App
store. Granting something this server cannot do would only produce a refusal the person could not explain.
</div>
</div>
)}
{/* Stated rather than silently omitted. An owner who cannot find the Terminal checkbox will assume
the screen is incomplete and go looking for it; saying why it does not exist is the difference
between a deliberate design and a missing feature. */}
<div className="flex gap-3 rounded-lg border border-dashed p-3 text-xs text-muted-foreground">
<Lock className="mt-0.5 h-4 w-4 shrink-0" />
<div>
<div className="font-medium text-foreground">Not listed, and not grantable</div>
Chat, tasks, capability authoring, the desktop and the browser run as the server owner, in the server
owner&rsquo;s home directory, with full permissions. Granting one would hand over the machine rather than a
feature, so there is no level at which they can be shared. The wallet, Headscale and the server settings stay
with the owner for the same reason.
<div className="mt-1.5">
Files is the exception, and only because of how it is built: a member with a Linux account on this machine
gets their own home, enforced by the operating system rather than by a check in the app.
</div>
</div>
</div>
{/* Two explanatory blocks used to sit here: one naming every capability whose sidecar is not installed,
and one naming everything that can never be granted. Both are gone, and for the same reason — a
server should not enumerate what it does not have. The first was a catalogue of uninstallable
features presented as a permissions decision; the second described chat, tasks, the desktop and the
wallet to an owner who may have none of them installed. What is on this screen is what this server
can actually do. */}
</div>
);
};