Modelled on the hook:* family rather than on geolocation:read. The host
dispatches INTO the child for every tool call, so it needs what
HOOK_PERMISSION buys: providersOf()'s active-and-holds-the-grant check,
the second gate in invokeHook, and the acting-user binding. That last one
is the point. The user is bound host-side from the supervisor's
invocation map, so a tool called by user A reaches A's data whatever the
model puts in args.
The permission keeps no hook: prefix because the surface it opens is MCP,
not a TREK render slot, and nothing keys off that prefix. It is the first
HOOK_PERMISSION value shaped that way; `satisfies` still holds.
One commit, because it cannot be split. requireTotalCoverage makes a
HOOK_PERMISSION key with no @PluginHook consumer a boot failure, not a
test failure, and PluginRpcRegistryService.onModuleInit runs in every
buildApp() — so a half-landed version breaks every integration test in the
repo. The generator is chained to it too: it refuses to run without both
the en admin label and the CLI hint.
15s is the longest budget of any read hook. The others back a render
someone is waiting on and fall back when they time out; this one backs a
chat turn, and the plugin is likely calling a third-party API of its own.
Still under the supervisor's unused 30s default, which is a hung client
rather than a budget.
One callTool fn for every tool, since tool names are runtime data and
hookContracts() is keyed by (hook, fn).
PluginDefinition exists in two copies with no generator between them, so
both get McpToolProvider. grantGaps and the dev mock host need no change:
both read HOOK_PERMISSION, which is generated.
PLUGHOOK-010, plus the CONTRACTS row.
* fix(plugins): enforce manifest apiVersion at install and activation
* test(plugins): exercise the absent-apiVersion default
* feat(plugins): generate the core-event catalog into host-facts
* docs(plugins): explain the packing.listBags write-permission binding
* test(plugins): cover apiVersion upload refusal; tidy docblocks and generator header
* fix(plugin-sdk): validate settings options and oauth like the host
* fix(plugin-sdk): model the full manifest shape and stop dropping fields
* fix(plugin-sdk): drop non-string routeProfiles icons like the host
* docs(plugin-sdk): document host limits, oauth contract, and ignored asUserId
* feat(plugin-sdk): enforce daily budgets and meta quotas in the mock host
* feat(plugin-sdk): expose the generated core-event catalog
* feat(plugin-sdk): scaffold trek range >=3.4.0 <5.0.0 ahead of TREK 4.0
* feat(plugin-sdk): scaffold the oauth broker settings for oauth:client plugins
* chore(plugin-sdk): fix stale mirror docs and the colliding permissions export
* docs(plugin-sdk): add the rate-limit string; restore trek docblock; match host meta sizing
* feat(plugin-sdk): raise the scaffold trek floor to 4.0.0
* docs(wiki): bring the plugin pages in line with the current runtime
* docs: bring the workspace CLAUDE.md guides up to date and make them committable
* feat(plugin-sdk): roll back failed publishes, add unrelease, sync the fork, retro-sign updates
* docs(wiki): document the publish rollback, unrelease, fork sync, and retro-signing
* chore(plugin-sdk): 1.6.0
The hook->permission map existed verbatim twice (the host supervisor and the
plugin SDK) and the permission list four times (envelope, SDK manifest, and
both admin panels in the client). All of them were in sync when measured -
63 entries each, 15 hook entries each - so this changes no behaviour. It
removes a quiet outage: a hook whose permission is added on only one side
fails silently in production, with the plugin installing, activating and then
simply never being called.
protocol/envelope.ts is the single source now. METHOD_PERMISSION was typed
Record<KnownMethod, string>, so its value side was unconstrained -
'db:read:trps' compiled and made the method unreachable forever. It is
`as const satisfies Record<KnownMethod, KnownPermission>` now, which catches a
typo'd value, a missing key and an extra key. The derived KnownPermission and
MethodPermission<M> are what the @PluginMethod decorator will check its
argument against.
Deliberately no explicit annotation on METHOD_PERMISSION: with one, typeof is
that Record, MethodPermission<M> widens to the whole union, and the check
silently evaporates.
server/scripts/gen-plugin-facts.ts imports envelope.ts and writes the SDK and
shared copies. It does not scrape. The guard it replaces pulled the block out
of plugin-supervisor.ts with a regex that goes null on a reformat, and it ran
only from prepublishOnly - drift would have surfaced as a broken npm release.
The generated files are checked in because publish-plugin-sdk builds from a
standalone checkout with no server present.
The SDK's re-exported types stay wide. plugin-sdk compiles under strict, and
grantGaps indexes HOOK_PERMISSION with a plain string, so an `as const` there
is TS7053 and would break the publish. Strictness belongs on the host side.
--check also fails on a permission with no admin.plugins.perm.* translation
and on one the CLI has no hint for. Both are silent today.
plugin-sdk/** was in no workflow path filter, so nothing in CI ever ran its
tests. It is in the filter now, alongside a job that runs the drift check.