two gaps between "a plugin can mount routes" and "a plugin is part of the platform". both closed. FIRST: nothing registered a plugin's declared permissions, so the capability gate could not resolve a plugin path at all. it resolved to null, and null is denied — the owner never noticed because isSuperAdmin short-circuits every check, which is exactly the shape of bug that reaches a member first. the registry is now rebuildable the same way the hono app is: CORE_REGISTRY holds the platform's own, CAPABILITIES is core plus whatever the installed plugins declare, and setPluginCapabilities replaces the plugin half wholesale rather than diffing it. two invariants hold by construction — DEFAULT_ROLE_- CAPABILITIES and CORE_CAPABILITIES derive from CORE_REGISTRY, so a plugin can never put itself in the fresh-install baseline and can never become `core` (every account, undeniable). a key colliding with a core one is refused and logged, because a plugin able to redefine `chat` could widen it. ownerOnly maps to admin, everything else to app. those are the only kinds a manifest can express, and it has no field for a kind at all. capabilities are registered BEFORE routes are mounted: the gate runs ahead of every router, so mounting a route whose permission is not yet registered would 403 the freshly installed plugin until something else happened to refresh. SECOND: nothing mounted plugins at boot. honoServer is built with none at import, because discovery reads disk and database and neither can be awaited at module scope, and every install verb rebuilt — so it tested perfectly and would have silently unmounted everything on the first restart. server.tsx now refreshes before serve(), so there is no window where an installed plugin 404s, and a plugin that will not load is logged rather than fatal. verified: after a restart, [plugins] mounted /example, the row survived, officer-example came back online from its ecosystem entry, capabilityForApiPath resolves /api/example/ping to the example capability at kind=app, and it appears in the owner's grantable list. 756 pass, same 10 pre-existing failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
import type { PluginManifest } from '@@/plugins/manifest';
|
|
|
|
// The reference plugin. Not a fixture — this is what a plugin author reads first, and it is deliberately
|
|
// the smallest thing that is still a real one: a manifest and one route.
|
|
//
|
|
// Everything structural is convention, so this directory IS the documentation:
|
|
//
|
|
// manifest.ts you are here — only what a directory listing cannot say
|
|
// api/router.ts exports `router`; mounted at /api/example
|
|
// db/schema.ts tables, if it had any (every name prefixed `example_`)
|
|
// sidecar/index.ts a process, if it needed one (.mjs instead means node)
|
|
// web/Router.tsx a frontend, if it had one
|
|
//
|
|
// `appName` is not declared anywhere: it is the directory name, so the id cannot disagree with where the
|
|
// code sits.
|
|
export const manifest: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '1.0.0',
|
|
platform: '>=1.0.0',
|
|
|
|
label: 'Example',
|
|
summary: 'The reference plugin — one route, nothing else',
|
|
icon: 'Puzzle',
|
|
color: '#94a3b8',
|
|
|
|
// One permission gating the whole surface. `ownerOnly: false` means a role can be granted it — which is
|
|
// the interesting case, because it is the one the capability gate actually has to resolve.
|
|
permissions: [
|
|
{
|
|
key: 'example',
|
|
label: 'Example',
|
|
description: 'The reference plugin',
|
|
ownerOnly: false,
|
|
},
|
|
],
|
|
};
|