switch off every plugin router, pending extraction

Twelve capabilities unmounted: gitea, music, photos, jellyfin, memos, calendar,
email, notify, transmission, soulseek, invoices, wallet. Mounts commented in
place, implementation untouched on disk, same as vault and the browser relay.

Each needed its capability's `api` claim commented in the same change.
assertCapabilityTotality check 2 refuses to boot on a capability claiming a
prefix nothing mounts, so unmounting alone would have stopped the server
starting — the opposite of what happened with vault, which is exempt.

music also claims two websockets. Its `ws: ['cliamp', 'cliamp-audio']` claim, the
two handlers in server.tsx and the provider union entries all had to move
together: check 3 fails on a served socket nothing claims, check 4 on a claimed
socket nothing serves.

calendar was the awkward one and the mount list was not where it lived. Four of
its six doors are not in the routes table:

  honoServer.route('/dav', davSyncRouter)     the sync door, top-level, not /api
  honoServer.all('/.well-known/caldav')       RFC 6764 autodiscovery
  honoServer.all('/.well-known/carddav')      the same for contacts
  four routes in server.tsx                   /dav, /dav/*, and both well-knowns

and the two that ARE in the table carry trailing comments, which is why the first
pass silently missed them and left /api/caldav and /api/dav mounted with their
claim gone — exactly the boot failure this commit is about.

/vpn deliberately stays. It reads as a plugin (kind 'app') but it is OffTail
enrollment forwarding to officer-headscale, which is core because the tailnet is
the perimeter.

Verified by simulating assertCapabilityTotality against the post-change files
rather than trusting the edits: parse hono's live mounts, the registry's live
claims, server.tsx's live ws providers and totality's two exempt lists, then run
all four checks. It reported the two stale caldav mounts, which is how they were
found. Zero problems now, 34 live mounts, all core.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:13:16 +00:00
co-authored by Claude Opus 5
parent 1eecc400a7
commit ffca309a77
3 changed files with 63 additions and 51 deletions
+8 -8
View File
@@ -41,8 +41,8 @@ type WSData = {
| 'chat'
| 'task-runner'
| 'pipeline'
| 'cliamp'
| 'cliamp-audio'
// | 'cliamp'
// | 'cliamp-audio'
| 'desktop'
// | 'vault'
| 'sidecar';
@@ -135,8 +135,8 @@ const handlers: Record<string, any> = {
chat: chatWebsocket,
'task-runner': taskRunnerWebsocket,
pipeline: pipelineWebsocket,
cliamp: cliampWebsocket,
'cliamp-audio': cliampAudioWebsocket,
// cliamp: cliampWebsocket,
// 'cliamp-audio': cliampAudioWebsocket,
desktop: desktopWebsocket,
// vault: vaultWebsocket,
sidecar: sidecarWebsocket,
@@ -300,10 +300,10 @@ const server = serve({
// fixed, spec-defined paths — `/.well-known/caldav` unauthenticated, before they hold any
// credential at all. They need naming explicitly here or the `/*` SPA fallback below swallows them
// and the phone gets an HTML page where it expected a redirect.
'/.well-known/caldav': honoServer.fetch,
'/.well-known/carddav': honoServer.fetch,
'/dav': honoServer.fetch,
'/dav/*': honoServer.fetch,
// '/.well-known/caldav': honoServer.fetch,
// '/.well-known/carddav': honoServer.fetch,
// '/dav': honoServer.fetch,
// '/dav/*': honoServer.fetch,
// Vaultwarden for third-party Bitwarden clients, unauthenticated at Officer's layer. Needs its own
// entry for the same reason /dav does: only the paths listed here reach hono, and anything else
// falls through to the SPA — which answers 200 with the React shell, so a missing line here looks
+25 -13
View File
@@ -126,7 +126,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Gitea',
description: 'Repositories, issues, pull requests and notifications from your own Gitea account',
kind: 'app',
api: ['/gitea'],
// api: ['/gitea'], // plugin — switched off 2026-08-13
api: [],
routes: ['/gitea'],
// Every Gitea call is already scoped to the caller's own personal access token — the sidecar resolves
// the token from the caller's row and the instance from the owner's, so a member holding `read` still
@@ -143,8 +144,9 @@ export const CAPABILITIES: Capability[] = [
label: 'Music',
description: 'The music library, playback, and your own favourites and playlists',
kind: 'app',
api: ['/music'],
ws: ['cliamp', 'cliamp-audio'],
// api: ['/music'], // plugin — switched off 2026-08-13
api: [],
// ws: ['cliamp', 'cliamp-audio'], // plugin — switched off 2026-08-13
routes: ['/music'],
// Already per-caller in the sidecar contract (X-Officer-User), which is what makes them safe to write
// at read level. The library itself — scanning, tags, file moves — is not, and is not listed.
@@ -155,7 +157,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Photos',
description: 'Browse the photo library',
kind: 'app',
api: ['/photos'],
// api: ['/photos'], // plugin — switched off 2026-08-13
api: [],
routes: ['/photos'],
},
{
@@ -163,7 +166,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Video',
description: 'Browse and play the Jellyfin library',
kind: 'app',
api: ['/jellyfin'],
// api: ['/jellyfin'], // plugin — switched off 2026-08-13
api: [],
routes: ['/jellyfin'],
},
{
@@ -171,7 +175,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Memos',
description: 'Notes',
kind: 'app',
api: ['/memos'],
// api: ['/memos'], // plugin — switched off 2026-08-13
api: [],
routes: ['/memos'],
},
{
@@ -179,7 +184,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Calendar and contacts',
description: 'Calendars, contacts, and the app passwords that sync them to a phone',
kind: 'app',
api: ['/caldav', '/dav'],
// api: ['/caldav', '/dav'], // plugin — switched off 2026-08-13
api: [],
routes: ['/calendar', '/contacts'],
// App passwords are minted for and revoked by their own owner; `/dav` holds nothing shared.
personal: ['/'],
@@ -189,7 +195,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Email',
description: 'Mail accounts and messages',
kind: 'app',
api: ['/email'],
// api: ['/email'], // plugin — switched off 2026-08-13
api: [],
routes: ['/email'],
},
{
@@ -197,7 +204,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Notifications',
description: 'Push notifications to your devices',
kind: 'app',
api: ['/notify'],
// api: ['/notify'], // plugin — switched off 2026-08-13
api: [],
// Device registration is the caller's own — a phone subscribing to its own push channel.
personal: ['/devices', '/subscriptions'],
},
@@ -206,7 +214,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Transmission',
description: 'Torrent downloads',
kind: 'app',
api: ['/transmission'],
// api: ['/transmission'], // plugin — switched off 2026-08-13
api: [],
routes: ['/transmission'],
},
{
@@ -214,7 +223,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Soulseek',
description: 'Search and download from the Soulseek network',
kind: 'app',
api: ['/slskd'],
// api: ['/slskd'], // plugin — switched off 2026-08-13
api: [],
routes: ['/soulseek'],
},
{
@@ -222,7 +232,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Invoices',
description: 'InvoiceShelf books',
kind: 'app',
api: ['/invoiceshelf'],
// api: ['/invoiceshelf'], // plugin — switched off 2026-08-13
api: [],
routes: ['/invoices'],
},
{
@@ -387,7 +398,8 @@ export const CAPABILITIES: Capability[] = [
label: 'Wallet',
description: "The server owner's bitcoin",
kind: 'admin',
api: ['/wallet'],
// api: ['/wallet'], // plugin — switched off 2026-08-13
api: [],
routes: ['/wallet'],
},
];
+30 -30
View File
@@ -19,27 +19,27 @@ import { uploadRouter } from './api/upload/upload';
import { settingsRouter } from './api/settings/settings';
import { dashboardsRouter } from './api/dashboards';
import { router as fileBrowserRouter } from './api/file-browser/router';
import { musicRouter } from './api/music/router';
// import { musicRouter } from './api/music/router';
// import { vaultRouter } from './api/vault/router';
// import { publicVaultRouter, VAULT_ONLY_PREFIXES, isBitwardenClient } from './api/vault/public-router';
import { agentHandoffRouter } from './api/agent-handoff/router';
import { slskdRouter } from './api/slskd/router';
// import { slskdRouter } from './api/slskd/router';
import { headscaleRouter } from './api/headscale/router';
import { transmissionRouter } from './api/transmission/router';
import { invoiceshelfRouter } from './api/invoiceshelf/router';
import { jellyfinRouter } from './api/jellyfin/router';
import { photosRouter } from './api/photos/router';
import { walletRouter } from './api/wallet/router';
// import { transmissionRouter } from './api/transmission/router';
// import { invoiceshelfRouter } from './api/invoiceshelf/router';
// import { jellyfinRouter } from './api/jellyfin/router';
// import { photosRouter } from './api/photos/router';
// import { walletRouter } from './api/wallet/router';
import { vpnRouter } from './api/vpn/router';
import { terminalRouter } from './api/terminal/sidecar-server';
import { caldavRouter } from './api/dav/sidecar-server';
import { memosRouter } from './api/memos/router';
import { giteaRouter } from './api/gitea/router';
// import { caldavRouter } from './api/dav/sidecar-server';
// import { memosRouter } from './api/memos/router';
// import { giteaRouter } from './api/gitea/router';
import { appStoreRouter } from './api/app-store/router';
import { davSyncRouter } from './api/dav/sync-router';
import { davRouter } from './api/dav/router';
// import { davSyncRouter } from './api/dav/sync-router';
// import { davRouter } from './api/dav/router';
import { claimIosProfile } from './api/dav/ios-profile';
import { notifyRouter } from './api/notify/router';
// import { notifyRouter } from './api/notify/router';
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
import { activityRouter } from './api/activity/router';
// Vault still hand-rolls its port capture, so it keeps a side-effect import; every other HTTP sidecar
@@ -48,7 +48,7 @@ import { activityRouter } from './api/activity/router';
import { dockRouter } from './api/dock/dock';
import { integrationsRouter, googleCallbackHandler } from './api/integrations/integrations';
import { queueRouter } from './api/queue/queue';
import { emailRouter } from './api/email/router';
// import { emailRouter } from './api/email/router';
// The browser relay is switched off — see server.tsx. Restoring this mount means restoring the
// registry's claim on '/browser' in the same commit, or assertCapabilityTotality refuses to boot.
// import { browserRouter } from './api/browser/router';
@@ -158,7 +158,7 @@ honoServer.get('/dav/provision/:file', (ctx) => {
});
});
honoServer.route('/dav', davSyncRouter);
// honoServer.route('/dav', davSyncRouter); // plugin — switched off 2026-08-13
// Autodiscovery. This is most of what makes adding an account on a phone feel transparent instead of
// fiddly: the client is given a bare domain and probes these paths UNAUTHENTICATED before it has any
@@ -168,8 +168,8 @@ honoServer.route('/dav', davSyncRouter);
// `.all`, not `.get`: RFC 6764 §6 has the client probe the well-known URI with the method it actually
// wants to use, and iOS sends PROPFIND, not GET. Registered as GET-only these answered 404 to every real
// client while looking perfectly healthy in a browser.
honoServer.all('/.well-known/caldav', (ctx) => ctx.redirect('/dav/', 301));
honoServer.all('/.well-known/carddav', (ctx) => ctx.redirect('/dav/', 301));
// honoServer.all('/.well-known/caldav', (ctx) => ctx.redirect('/dav/', 301));
// honoServer.all('/.well-known/carddav', (ctx) => ctx.redirect('/dav/', 301));
const protectedRouter = createRouter();
protectedRouter.use(bodyParser());
@@ -199,28 +199,28 @@ const PROTECTED_MOUNTS: [prefix: string, router: ReturnType<typeof createRouter>
['/api-keys', apiKeysRouter], // your own keys; the `account` core capability covers it
['/dashboards', dashboardsRouter],
['/file-browser', fileBrowserRouter],
['/music', musicRouter],
['/slskd', slskdRouter],
// ['/music', musicRouter], // plugin — switched off 2026-08-13
// ['/slskd', slskdRouter], // plugin — switched off 2026-08-13
['/terminal', terminalRouter],
['/memos', memosRouter],
['/gitea', giteaRouter],
// ['/memos', memosRouter], // plugin — switched off 2026-08-13
// ['/gitea', giteaRouter], // plugin — switched off 2026-08-13
['/app-store', appStoreRouter],
['/caldav', caldavRouter], // the JSON door for Officer's own calendar/contacts UI
['/dav', davRouter], // app-password management (the sync door is /dav, top-level)
['/notify', notifyRouter],
// ['/caldav', caldavRouter], // the JSON door for Officer's own calendar/contacts UI — plugin, switched off 2026-08-13
// ['/dav', davRouter], // app-password management (the sync door is /dav, top-level) — plugin, switched off
// ['/notify', notifyRouter], // plugin — switched off 2026-08-13
['/headscale', headscaleRouter],
['/transmission', transmissionRouter],
['/invoiceshelf', invoiceshelfRouter],
['/jellyfin', jellyfinRouter],
['/photos', photosRouter],
['/wallet', walletRouter],
// ['/transmission', transmissionRouter], // plugin — switched off 2026-08-13
// ['/invoiceshelf', invoiceshelfRouter], // plugin — switched off 2026-08-13
// ['/jellyfin', jellyfinRouter], // plugin — switched off 2026-08-13
// ['/photos', photosRouter], // plugin — switched off 2026-08-13
// ['/wallet', walletRouter], // plugin — switched off 2026-08-13
['/vpn', vpnRouter],
['/system-monitor', systemMonitorRouter],
['/activity', activityRouter],
['/dock', dockRouter],
['/integrations', integrationsRouter],
['/queue', queueRouter],
['/email', emailRouter],
// ['/email', emailRouter], // plugin — switched off 2026-08-13
// ['/browser', browserRouter], // switched off — see server.tsx
['/bug-report', bugReportRouter],
['/agent-status', agentStatusRouter],