Skip to content

Commit e7247b7

Browse files
os-zhuangclaude
andcommitted
fix(rest): make requiresService nav gate fire in single-kernel deployments (ADR-0057 D10)
resolveExecCtx leaves kernel undefined on the single-kernel fallback path (no kernelManager), so ctx.__kernel was undefined → resolveRegisteredServices returned null → the gate failed OPEN (Organizations still shown in single-tenant). This is the primary OSS deployment mode (and the verify harness). Add a single-env serviceExistsProvider wired from the plugin's ctx.getService; resolveRegisteredServices now probes the per-request kernel (multi-env) OR that provider (single-env). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 790b357 commit e7247b7

2 files changed

Lines changed: 25 additions & 6 deletions

File tree

packages/rest/src/rest-api-plugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,13 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
185185

186186
ctx.logger.info('Hydrating REST API from Protocol...');
187187

188+
// Single-env service-existence probe for nav capability gates
189+
// (ADR-0057 D10). Multi-env uses the per-request kernel instead.
190+
const serviceExistsProvider = (name: string): boolean => {
191+
try { return ctx.getService<any>(name) != null; } catch { return false; }
192+
};
188193
try {
189-
const restServer = new RestServer(server, protocol, config.api as any, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider);
194+
const restServer = new RestServer(server, protocol, config.api as any, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
190195
restServer.registerRoutes();
191196

192197
ctx.logger.info('REST API successfully registered');

packages/rest/src/rest-server.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,10 @@ export class RestServer {
542542
private i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>;
543543
private analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>;
544544
private settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>;
545+
/** Sync probe: is a kernel service registered? Single-env path for nav
546+
* capability gates (ADR-0057 D10) — resolveExecCtx sets no kernel in
547+
* single-kernel deployments, so this prevents the gate failing open. */
548+
private serviceExistsProvider?: (name: string) => boolean;
545549

546550
constructor(
547551
server: IHttpServer,
@@ -560,6 +564,7 @@ export class RestServer {
560564
i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>,
561565
analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>,
562566
settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>,
567+
serviceExistsProvider?: (name: string) => boolean,
563568
) {
564569
this.protocol = protocol;
565570
this.config = this.normalizeConfig(config);
@@ -577,6 +582,7 @@ export class RestServer {
577582
this.i18nServiceProvider = i18nServiceProvider;
578583
this.analyticsServiceProvider = analyticsServiceProvider;
579584
this.settingsServiceProvider = settingsServiceProvider;
585+
this.serviceExistsProvider = serviceExistsProvider;
580586
}
581587

582588
/**
@@ -1151,7 +1157,18 @@ export class RestServer {
11511157
* behaviour). ADR-0057 addendum D10.
11521158
*/
11531159
private async resolveRegisteredServices(kernel: any, items: any[]): Promise<Set<string> | null> {
1154-
if (!kernel || typeof kernel.getServiceAsync !== 'function') return null;
1160+
// Prefer the per-request kernel (multi-env, resolved via kernelManager).
1161+
// Fall back to the single-env service-existence provider — in single-kernel
1162+
// deployments resolveExecCtx never sets a kernel, so without this the gate
1163+
// would fail open (ADR-0057 D10).
1164+
let probe: ((name: string) => Promise<boolean>) | null = null;
1165+
if (kernel && typeof kernel.getServiceAsync === 'function') {
1166+
probe = async (name) => { try { return (await kernel.getServiceAsync(name)) != null; } catch { return false; } };
1167+
} else if (this.serviceExistsProvider) {
1168+
const exists = this.serviceExistsProvider;
1169+
probe = async (name) => { try { return exists(name) === true; } catch { return false; } };
1170+
}
1171+
if (!probe) return null;
11551172
const wanted = new Set<string>();
11561173
const walk = (e: any): void => {
11571174
if (!e || typeof e !== 'object') return;
@@ -1163,10 +1180,7 @@ export class RestServer {
11631180
for (const it of items) walk(it);
11641181
if (wanted.size === 0) return new Set();
11651182
const registered = new Set<string>();
1166-
for (const name of wanted) {
1167-
try { if ((await kernel.getServiceAsync(name)) != null) registered.add(name); }
1168-
catch { /* service not registered -> leave out */ }
1169-
}
1183+
for (const name of wanted) { if (await probe(name)) registered.add(name); }
11701184
return registered;
11711185
}
11721186

0 commit comments

Comments
 (0)