You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
plugin-dev's "no security services" warning cannot fire for a SecurityPlugin whose start() bailed — the three slots it probes are registered in init(), before the bail #10036
Found while implementing #7616 (step 2, PR #10035), which had to establish exactly when the security service is and is not registered. Filed rather than fixed there: the defect is in plugin-dev / plugin-security, and #7616's file surface was ruled to be plugin-hono-server plus its pins.
The two registration phases are not the same phase
SecurityPlugin registers in init() (packages/plugins/plugin-security/src/security-plugin.ts:725-743):
and in start() (:1157, plus every ql.registerMiddleware call from :1166 on):
the published security service
all of the enforcement middleware
start() returns early, twice, before any of that (:794-812): once when objectql / metadata cannot be resolved, and once when if (!ql || typeof ql.registerMiddleware !== 'function'). Both log a warn and return.
So a stack in that state has all six init()-registered handles present and no enforcement whatsoever — no RBAC, no RLS, no field masking, and no security service.
What that breaks
packages/plugins/plugin-dev/src/dev-plugin.ts:897 probes precisely those handles to decide whether to warn:
…and the warning it guards says, in as many words, that "RBAC, row-level security and field masking are NOT enforced".
In the bailed-start state all three resolve, missing is empty, and the warning does not print — in the one state where its text is exactly, literally true. The check answers "is SecurityPlugin loaded?" when the question the warning asks is "is anything being enforced?", and those two came apart at start().
The comment above it is right about the principle it is defending ("a fake that answers allowed is worse than an absent one", #4126 / ADR-0076 D12); the probe just reads a signal that outlives the capability it signals. This is the AGENTS.md "absence must be loud" rule failing in the direction that is hardest to notice: silence read as health.
Why it is worth a card rather than a comment
The same presence signal misled a second consumer, which is how it was found. plugin-hono-server's /auth/me/permissions and /me/apps keyed their degraded branches on security.permissions and therefore, in this state, reported a restrictive permission map computed against enforcement that does not exist — the console hiding what the API serves. PR #10035 fixes that half by keying on the published security service instead, and measured the before/after; it does not touch this one.
Two consumers, two packages, same misread. That is a property of the signal, not of either reader.
Do not leave a half-initialised presence signal at all — have SecurityPlugin either not register the internal handles until start() succeeds, or register a boot-state handle that says which phase it reached. This is the contract-first shape but it is a behaviour change for anything that resolves those three today.
Make the bail itself loud at the composition level. Both early returns currently warn from inside the plugin; neither is visible to a consumer asking "is this stack enforcing anything?".
Worth noting for whoever picks this up: after #10035 lands, security.permissions / security.rls / security.fieldMasker have no non-test consumers left in this repo other than the dev-plugin.ts probe above (verified by grep across packages/ and apps/, excluding tests and the registrar itself). So option 2's blast radius inside this repo is small — though these are resolvable service names and may have out-of-repo consumers.
Unassigned and unlabelled — for the triage seat to grade and route.
Found while implementing #7616 (step 2, PR #10035), which had to establish exactly when the
securityservice is and is not registered. Filed rather than fixed there: the defect is inplugin-dev/plugin-security, and #7616's file surface was ruled to beplugin-hono-serverplus its pins.The two registration phases are not the same phase
SecurityPluginregisters ininit()(packages/plugins/plugin-security/src/security-plugin.ts:725-743):security.permissions,security.rls,security.fieldMaskersecurity.bootstrapPermissionSets,security.fallbackPermissionSet,security.baselinePermissionSetsand in
start()(:1157, plus everyql.registerMiddlewarecall from:1166on):securityservicestart()returns early, twice, before any of that (:794-812): once whenobjectql/metadatacannot be resolved, and once whenif (!ql || typeof ql.registerMiddleware !== 'function'). Both log awarnand return.So a stack in that state has all six
init()-registered handles present and no enforcement whatsoever — no RBAC, no RLS, no field masking, and nosecurityservice.What that breaks
packages/plugins/plugin-dev/src/dev-plugin.ts:897probes precisely those handles to decide whether to warn:…and the warning it guards says, in as many words, that "RBAC, row-level security and field masking are NOT enforced".
In the bailed-start state all three resolve,
missingis empty, and the warning does not print — in the one state where its text is exactly, literally true. The check answers "is SecurityPlugin loaded?" when the question the warning asks is "is anything being enforced?", and those two came apart atstart().The comment above it is right about the principle it is defending ("a fake that answers allowed is worse than an absent one", #4126 / ADR-0076 D12); the probe just reads a signal that outlives the capability it signals. This is the AGENTS.md "absence must be loud" rule failing in the direction that is hardest to notice: silence read as health.
Why it is worth a card rather than a comment
The same presence signal misled a second consumer, which is how it was found.
plugin-hono-server's/auth/me/permissionsand/me/appskeyed their degraded branches onsecurity.permissionsand therefore, in this state, reported a restrictive permission map computed against enforcement that does not exist — the console hiding what the API serves. PR #10035 fixes that half by keying on the publishedsecurityservice instead, and measured the before/after; it does not touch this one.Two consumers, two packages, same misread. That is a property of the signal, not of either reader.
Leads, not decisions
securityservice is registered only after both bails and only alongside the middleware, so its presence is the honest proxy. Cheapest fix, and it is the same move fix(plugin-hono-server): delegate permission-set resolution to the security service #10035 made.SecurityPlugineither not register the internal handles untilstart()succeeds, or register a boot-state handle that says which phase it reached. This is the contract-first shape but it is a behaviour change for anything that resolves those three today.warnfrom inside the plugin; neither is visible to a consumer asking "is this stack enforcing anything?".Worth noting for whoever picks this up: after #10035 lands,
security.permissions/security.rls/security.fieldMaskerhave no non-test consumers left in this repo other than thedev-plugin.tsprobe above (verified by grep acrosspackages/andapps/, excluding tests and the registrar itself). So option 2's blast radius inside this repo is small — though these are resolvable service names and may have out-of-repo consumers.Unassigned and unlabelled — for the triage seat to grade and route.