refuse a plugin install whose permission key the platform still claims

Three times in one day — photos, soulseek, jellyfin — the same failure on the
same machine: install reports success, the dock tile appears, the frontend
rebuilds with the route in it, and every request 404s. The plugin never mounted,
because setPluginPermissions refused its key as already core.

That refusal was correct and useless. It happens at mount time, after the
install has reported success, and its only trace is one line on stderr that
nobody is reading. The cause is always the same: the key was a CORE permission
until the extraction commit, so a server whose platform predates that commit
still owns it. The fix is always git pull, and nothing in the product said so.

installPlugin now checks the manifest's keys against the core registry before
anything is written, and refuses with a message naming the colliding keys and
telling the operator to pull the platform and restart. Placed with the other
pre-flight checks, so it changes nothing that would need undoing.

corePermissionCollisions is exported from the registry rather than reimplemented
in install.ts — the core key set has one owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 03:33:00 +00:00
co-authored by Claude Opus 5
parent aebd6ff0c0
commit ee321f49fd
3 changed files with 69 additions and 0 deletions
+25
View File
@@ -7,6 +7,7 @@ import {
setDockPaths,
setPluginEnabled,
} from 'officerdb';
import { corePermissionCollisions } from '../permissions/registry';
import { rm } from 'node:fs/promises';
import { PLATFORM_DIR } from '../data-path';
import { deleteProcess, processStatus, startProcess, stopProcess } from '../app-store/pm2';
@@ -169,6 +170,30 @@ export async function installPlugin(
}
}
// PERMISSION KEYS BEFORE ANYTHING IS WRITTEN.
//
// A key the platform still claims is refused by `setPluginPermissions` at mount time — correctly, but
// too late and too quietly: the install completes, the dock tile appears, the frontend rebuilds with
// the route in it, and every request 404s because the plugin never mounted. The only trace is one line
// on stderr.
//
// It is not a hypothetical. photos, soulseek and jellyfin each hit it within a day of being extracted,
// always the same way: the plugin's key was a CORE permission until the extraction commit, so any
// server whose platform predates that commit still claims it. The fix is always `git pull`, and the
// symptom never says so.
const collisions = corePermissionCollisions(plugin.manifest.permissions.map((p) => p.key));
if (collisions.length) {
return {
ok: false,
appName,
steps,
error:
`the platform still defines ${collisions.map((c: string) => `"${c}"`).join(', ')} as a core permission, ` +
`so "${appName}" cannot claim it. This server's platform predates the commit that extracted ` +
`${appName} — pull the platform and restart officer, then install again.`,
};
}
try {
// HOST BINARIES FIRST, before a table is created or a row is written.
//