the platform stops knowing what VNC is

Desktop is a plugin as of today, and this is the half that leaves. sidecar/vnc/, api/desktop/ and
apps/Desktop/ are gone from src/, along with the vocabulary that was the real problem.

sidecar/protocol.ts had a VNC section: vnc:start, vnc:ensure-password, vnc:stop, four reply variants
and two types. sidecar-registry.ts had startVnc, ensureVncPassword, stopVnc, isVncConnected. Four
functions and a dozen message types in the platform, describing a feature that is not the platform's —
the rule CLAUDE.md states most plainly, and its clearest breach.

What replaced them is generic. sendSidecarCommand(kind, cmd) and isSidecarConnected(kind) know that a
command has a type and an id and that a reply carries the same id. A plugin's sidecar speaks whatever
it likes through them and nothing here has to be edited when it adds a message.

The desktop PERMISSION leaves too, and that is the interesting part: it was kind: 'execution', a kind
a plugin permission cannot have and never will, because a permission nobody can delegate is a
contradiction. The plugin expresses the same refusal as ownerOnly: ['/'] — every path under it denied
to anyone but the owner. Verified: /api/desktop, /api/desktop/vnc-password and a path that does not
exist all resolve owner-only. registry.test.ts records the round trip rather than just dropping the
key from its list.

Also gone: the app store's `vnc` entry, which would have offered an older second way to install the
same thing against a process PM2 no longer has, and four commented-out mounts left from 2026-08-13.

The disconnect-scope test used startVnc as its stand-in for "another sidecar's long-running request".
It now uses the generic door, which is the better instrument — that file is about the blast radius of
a disconnect and never had anything to do with VNC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 00:42:19 +00:00
co-authored by Claude Opus 5
parent b1a2d67b76
commit fcdbf02649
20 changed files with 67 additions and 929 deletions
+6 -4
View File
@@ -212,10 +212,12 @@ describe('self-service routes', () => {
describe('kinds', () => {
test('execution permissions are never grantable', () => {
const grantable = new Set(GRANTABLE_PERMISSIONS.map((c) => c.key));
// `files` left this list on 2026-08-11, then `terminal` and `chat` the same day — see the tests below and
// docs/per-user-linux-accounts.md. Everything still here runs as the OWNER in the owner's home with no
// per-caller resolution at all.
for (const key of ['tasks', 'desktop', 'browser', 'items']) {
// `files` left this list on 2026-08-11, then `terminal` and `chat` the same day — see the tests below
// and docs/per-user-linux-accounts.md. `desktop` left on 2026-08-21, differently: it became a plugin,
// and a plugin permission has no kind at all. What made it execution — one physical screen, the
// owner's — is now `ownerOnly: ['/']` in that plugin's manifest, which is the same refusal reached by
// a different route. Everything still here runs as the OWNER in the owner's home.
for (const key of ['tasks', 'browser', 'items']) {
expect(PERMISSION_BY_KEY.get(key)?.kind).toBe('execution');
expect(grantable.has(key)).toBe(false);
}
+4 -10
View File
@@ -322,16 +322,10 @@ const CORE_REGISTRY: Permission[] = [
kind: 'execution',
api: ['/skills', '/tools', '/agents', '/processes', '/rescan'],
},
{
key: 'desktop',
label: 'Desktop',
description: "The server owner's physical screen",
kind: 'execution',
// api: ['/desktop'], // plugin — switched off 2026-08-13
// ws: ['desktop'],
api: [],
routes: ['/desktop'],
},
// `desktop` left with the plugin on 2026-08-21. It is the first permission to make the round trip from
// `kind: 'execution'` to a plugin's own, and the property survived the move: a plugin permission has no
// kind, so "runs as the owner and cannot be shared" is expressed as `ownerOnly: ['/']` in the plugin's
// manifest instead. Same answer, stated where the paths are.
// `/browser` — the Chrome-extension relay — is unmounted as of 2026-08-13 and dropped from this claim,
// because check 2 of assertPermissionTotality refuses to boot on a permission claiming a prefix nothing
// serves. Put both back together or neither.