Symptom
AuthPlugin binds four things to the live kernel on kernel:ready:
line (packages/plugins/plugin-auth/src/auth-plugin.ts) |
binding |
| 739 |
authManager.setEmailService(ctx.getService('email')) — the outbound mail transport |
| 834 |
authManager.setDefaultEmailLocale(...) — the #8195 / #14319 deployment locale rung |
| 862 |
authManager.setAppName(...) — branding.workspace_name |
| 889 |
authManager.setDefaultSmsLocale(...) — the #2815 SMS locale |
All four sit inside one hook, and that hook is gated on a flag about HTTP routing:
// auth-plugin.ts:727
if (this.options.registerRoutes) {
ctx.hook('kernel:ready', async () => { // 728
... // 734..978 — all four bindings
}); // 979
} // 980
registerRoutes answers "should this plugin mount its own /api/v1/auth/* routes on the kernel's http-server". It is a transport-mounting question. Wiring the mail service, the deployment email locale, the brand name and the SMS locale are service-composition questions, and they are true of an embedding regardless of who serves the routes.
So an embedding that serves auth routes itself — the whole point of registerRoutes: false — silently gets none of them. The auth manager comes up with no mail transport, no locale on either channel, and no brand binding, and nothing logs a word about it: the four ctx.logger.info lines that would say so are inside the same skipped block.
Who this is
objectstack-ai/cloud is the live one. Every tenant environment kernel constructs AuthPlugin with registerRoutes: false (packages/objectos-runtime/src/artifact-kernel-factory.ts:728) because the per-env kernel has no http-server — the host worker's AuthProxyPlugin forwards /api/v1/auth/* to the env's better-auth handler. That is the intended, documented arrangement, and cloud has already had to re-mount one other casualty of the same gate by hand (the OAuth/OIDC discovery documents, cloud#1339).
The result on the hosted product: the #14591 ruling — localization.locale outranks build-time i18n.defaultLocale for auth mail — cannot reach a tenant environment at all, because the binding that carries it never runs there. That reconciliation is objectstack-ai/cloud#1857, and it cannot be satisfied on the cloud side without either duplicating this resolution logic (a second authority, which #14591 exists to prevent) or reaching into plugin internals.
Measured, not read
Against this repo at 655b106c (the SHA cloud currently pins), same harness shape as auth-plugin.test.ts's existing #14319 block, with an EXPLICIT localization.locale = zh-CN settings double and an email service present, varying only registerRoutes:
registerRoutes=TRUE -> {"settingsRead":[["localization","locale",{}]],
"emailLocaleCalls":[["en"],["zh-CN"]],
"smsLocaleCalls":[["zh-CN"]],
"emailServiceCalls":[[{}]],"appNameCalls":[[null]]}
registerRoutes=FALSE -> {"settingsRead":[],"emailLocaleCalls":[],"smsLocaleCalls":[],
"emailServiceCalls":[],"appNameCalls":[]}
With registerRoutes: false the settings namespace is not even read. The same structure holds in the shipped artifact, not only in source: brace-matching dist/index.js puts all four call sites inside the registerRoutes block (offsets 473307 / 475311 / 476100 / 476982 within block 472971..479750).
Confirmed again on a real boot of the cloud rig at that pin — one process pair, two kernels:
cloud.log (control plane, registerRoutes defaults true)
INFO Auth: email service wired (transactional mail enabled)
INFO Auth: bound auth email locale to i18n default=en
INFO Auth: bound appName to settings namespace=branding
objectos.log (tenant env kernel, registerRoutes:false) — zero matches
...though the same log shows the kernel DID build with auth on:
INFO Initializing Auth Plugin... / Auth Plugin initialized successfully
INFO [ArtifactKernelFactory] kernel ready {... "authEnabled":true}
Corroborating third reading: that env answers requireEmailVerification: false at GET /api/v1/auth/config, which is what resolveRequireEmailVerification() returns when it sees no transport — the absent setEmailService showing through a second surface.
Suggested shape
Split the hook: keep route registration under if (this.options.registerRoutes), and move the four service bindings (plus ensureAuthSettingsBound) into their own ctx.hook('kernel:ready', ...) registered unconditionally — the shape the sibling hooks at auth-plugin.ts:993 / :1032 already use, and which their comments already name ("Registered independently of registerRoutes so an embedding that serves no auth routes still gets the diagnosis"). The diagnosis hook got that treatment; the wiring it diagnoses did not.
That is one edit in one file, and it is where the authority already lives — the alternative is every routes-less host re-deriving localization.locale precedence for itself, which is the second authority #14591 removed.
Worth deciding as part of the same change: whether the "no email service registered" branch (auth-plugin.ts:745-758) should keep logging at info, given it is now reachable on hosts that never see it today.
Acceptance
- A kernel with
registerRoutes: false, a settings service answering an explicit localization.locale, and an email service, ends kernel:ready with the transport wired and both locales bound — asserted by extending the #14319 describe block in auth-plugin.test.ts to cover both values of registerRoutes rather than only the default.
- Route registration itself stays gated: a
registerRoutes: false kernel still mounts no auth routes.
Cloud-side reconciliation this unblocks: objectstack-ai/cloud#1857.
Generated by Claude Code
Symptom
AuthPluginbinds four things to the live kernel onkernel:ready:packages/plugins/plugin-auth/src/auth-plugin.ts)authManager.setEmailService(ctx.getService('email'))— the outbound mail transportauthManager.setDefaultEmailLocale(...)— the #8195 / #14319 deployment locale rungauthManager.setAppName(...)—branding.workspace_nameauthManager.setDefaultSmsLocale(...)— the #2815 SMS localeAll four sit inside one hook, and that hook is gated on a flag about HTTP routing:
registerRoutesanswers "should this plugin mount its own/api/v1/auth/*routes on the kernel'shttp-server". It is a transport-mounting question. Wiring the mail service, the deployment email locale, the brand name and the SMS locale are service-composition questions, and they are true of an embedding regardless of who serves the routes.So an embedding that serves auth routes itself — the whole point of
registerRoutes: false— silently gets none of them. The auth manager comes up with no mail transport, no locale on either channel, and no brand binding, and nothing logs a word about it: the fourctx.logger.infolines that would say so are inside the same skipped block.Who this is
objectstack-ai/cloudis the live one. Every tenant environment kernel constructsAuthPluginwithregisterRoutes: false(packages/objectos-runtime/src/artifact-kernel-factory.ts:728) because the per-env kernel has nohttp-server— the host worker'sAuthProxyPluginforwards/api/v1/auth/*to the env's better-auth handler. That is the intended, documented arrangement, and cloud has already had to re-mount one other casualty of the same gate by hand (the OAuth/OIDC discovery documents, cloud#1339).The result on the hosted product: the #14591 ruling —
localization.localeoutranks build-timei18n.defaultLocalefor auth mail — cannot reach a tenant environment at all, because the binding that carries it never runs there. That reconciliation isobjectstack-ai/cloud#1857, and it cannot be satisfied on the cloud side without either duplicating this resolution logic (a second authority, which #14591 exists to prevent) or reaching into plugin internals.Measured, not read
Against this repo at
655b106c(the SHA cloud currently pins), same harness shape asauth-plugin.test.ts's existing#14319block, with an EXPLICITlocalization.locale = zh-CNsettings double and anemailservice present, varying onlyregisterRoutes:With
registerRoutes: falsethe settings namespace is not even read. The same structure holds in the shipped artifact, not only in source: brace-matchingdist/index.jsputs all four call sites inside theregisterRoutesblock (offsets 473307 / 475311 / 476100 / 476982 within block 472971..479750).Confirmed again on a real boot of the cloud rig at that pin — one process pair, two kernels:
Corroborating third reading: that env answers
requireEmailVerification: falseatGET /api/v1/auth/config, which is whatresolveRequireEmailVerification()returns when it sees no transport — the absentsetEmailServiceshowing through a second surface.Suggested shape
Split the hook: keep route registration under
if (this.options.registerRoutes), and move the four service bindings (plusensureAuthSettingsBound) into their ownctx.hook('kernel:ready', ...)registered unconditionally — the shape the sibling hooks at auth-plugin.ts:993 / :1032 already use, and which their comments already name ("Registered independently ofregisterRoutesso an embedding that serves no auth routes still gets the diagnosis"). The diagnosis hook got that treatment; the wiring it diagnoses did not.That is one edit in one file, and it is where the authority already lives — the alternative is every routes-less host re-deriving
localization.localeprecedence for itself, which is the second authority #14591 removed.Worth deciding as part of the same change: whether the "no email service registered" branch (auth-plugin.ts:745-758) should keep logging at
info, given it is now reachable on hosts that never see it today.Acceptance
registerRoutes: false, asettingsservice answering an explicitlocalization.locale, and anemailservice, endskernel:readywith the transport wired and both locales bound — asserted by extending the#14319describe block inauth-plugin.test.tsto cover both values ofregisterRoutesrather than only the default.registerRoutes: falsekernel still mounts no auth routes.Cloud-side reconciliation this unblocks:
objectstack-ai/cloud#1857.Generated by Claude Code