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:
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { corePermissionCollisions } from './registry';
|
||||
|
||||
// The failure this exists to prevent, in one sentence: a plugin whose permission key the platform still
|
||||
// claims installs "successfully", shows a dock tile, rebuilds the frontend with its route — and 404s on
|
||||
// every request, because `setPluginPermissions` refused the key at mount time and said so only on stderr.
|
||||
//
|
||||
// It caught photos, soulseek and jellyfin on the same machine inside a day. Each had been a CORE
|
||||
// permission until its extraction commit, so any server whose platform predates that commit still owns
|
||||
// the key. The remedy is always `git pull`, and nothing in the product ever said so.
|
||||
|
||||
describe('core permission collisions', () => {
|
||||
test('names a key the platform still claims', () => {
|
||||
// 'chat' is core and not deniable — the clearest case of something a plugin must never redefine.
|
||||
expect(corePermissionCollisions(['chat'])).toEqual(['chat']);
|
||||
});
|
||||
|
||||
test('an ordinary plugin key collides with nothing', () => {
|
||||
expect(corePermissionCollisions(['soulseek', 'jellyfin', 'photos'])).toEqual([]);
|
||||
});
|
||||
|
||||
test('reports every collision, not just the first — an install should fail once, naming all of them', () => {
|
||||
const found = corePermissionCollisions(['chat', 'soulseek', 'terminal']);
|
||||
expect(found.sort()).toEqual(['chat', 'terminal']);
|
||||
});
|
||||
|
||||
test('nothing declared collides with nothing', () => {
|
||||
expect(corePermissionCollisions([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -460,6 +460,20 @@ export const CORE_PERMISSIONS = CORE_REGISTRY.filter((c) => c.kind === 'core');
|
||||
* question. A key colliding with a core one is REFUSED here rather than silently overriding — a plugin
|
||||
* that could redefine `chat` or `terminal` could widen it.
|
||||
*/
|
||||
/**
|
||||
* Which of these keys the PLATFORM already claims.
|
||||
*
|
||||
* Exported so install can refuse before it starts, rather than leaving `setPluginPermissions` to reject
|
||||
* the key afterwards — that rejection is real but it happens at mount time and only reaches stderr, so
|
||||
* the install reports success, the dock tile appears, and the route 404s. That exact sequence caught
|
||||
* photos, soulseek and jellyfin on the same machine within a day: each had been a CORE permission until
|
||||
* the extraction commit, so any server whose platform predates it still claims the key.
|
||||
*/
|
||||
export function corePermissionCollisions(keys: string[]): string[] {
|
||||
const coreKeys = new Set(CORE_REGISTRY.map((c) => c.key));
|
||||
return keys.filter((k) => coreKeys.has(k));
|
||||
}
|
||||
|
||||
export function setPluginPermissions(pluginPermissions: Permission[]): { rejected: string[] } {
|
||||
const coreKeys = new Set(CORE_REGISTRY.map((c) => c.key));
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user