diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 89187389c..6b864b03b 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -276,6 +276,7 @@ export class Kernel { this.#kernelServiceManager.registerKernelServiceObject( 'kernelFacet', kernelFacet, + { systemOnly: true }, ); return kernelFacet; } @@ -367,10 +368,21 @@ export class Kernel { * * @param name - The name of the service. * @param object - The service object to register. + * @param options - Registration options. + * @param options.systemOnly - Whether the service is only available to system + * subclusters. Defaults to `false`. * @returns The registration details including the kref. */ - registerKernelServiceObject(name: string, object: object): KernelService { - return this.#kernelServiceManager.registerKernelServiceObject(name, object); + registerKernelServiceObject( + name: string, + object: object, + options?: { systemOnly?: boolean }, + ): KernelService { + return this.#kernelServiceManager.registerKernelServiceObject( + name, + object, + options, + ); } /** diff --git a/packages/ocap-kernel/src/KernelServiceManager.test.ts b/packages/ocap-kernel/src/KernelServiceManager.test.ts index eacee0d31..cec435293 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.test.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.test.ts @@ -59,6 +59,30 @@ describe('KernelServiceManager', () => { expect(registered.name).toBe('testService'); expect(registered.kref).toMatch(/^ko\d+$/u); expect(registered.service).toBe(testService); + expect(registered.systemOnly).toBe(false); + }); + + it('defaults systemOnly to false when no options provided', () => { + const testService = { testMethod: () => 'test' }; + + const registered = serviceManager.registerKernelServiceObject( + 'testService', + testService, + ); + + expect(registered.systemOnly).toBe(false); + }); + + it('sets systemOnly to true when specified', () => { + const testService = { testMethod: () => 'test' }; + + const registered = serviceManager.registerKernelServiceObject( + 'testService', + testService, + { systemOnly: true }, + ); + + expect(registered.systemOnly).toBe(true); }); it('pins the service object in kernel store', () => { @@ -123,6 +147,18 @@ describe('KernelServiceManager', () => { expect(retrieved).toStrictEqual(registered); }); + it('returns the systemOnly flag on retrieved service', () => { + const testService = { testMethod: () => 'test' }; + + serviceManager.registerKernelServiceObject('sysOnly', testService, { + systemOnly: true, + }); + serviceManager.registerKernelServiceObject('open', testService); + + expect(serviceManager.getKernelService('sysOnly')?.systemOnly).toBe(true); + expect(serviceManager.getKernelService('open')?.systemOnly).toBe(false); + }); + it('returns undefined for non-existent service', () => { const retrieved = serviceManager.getKernelService('nonExistent'); expect(retrieved).toBeUndefined(); diff --git a/packages/ocap-kernel/src/KernelServiceManager.ts b/packages/ocap-kernel/src/KernelServiceManager.ts index 4741f6255..3208984ab 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.ts @@ -10,6 +10,7 @@ export type KernelService = { name: string; kref: string; service: object; + systemOnly: boolean; }; type KernelServiceManagerConstructorProps = { @@ -60,9 +61,16 @@ export class KernelServiceManager { * * @param name - The name of the service. * @param service - The service object. + * @param options - Registration options. + * @param options.systemOnly - Whether the service is only available to system + * subclusters. Defaults to `false`. * @returns The registered kernel service with its kref. */ - registerKernelServiceObject(name: string, service: object): KernelService { + registerKernelServiceObject( + name: string, + service: object, + { systemOnly = false }: { systemOnly?: boolean } = {}, + ): KernelService { if (this.#kernelServicesByName.has(name)) { throw new Error(`Kernel service "${name}" is already registered`); } @@ -73,7 +81,7 @@ export class KernelServiceManager { this.#kernelStore.kv.set(serviceKey, kref); this.#kernelStore.pinObject(kref); } - const kernelService = { name, kref, service }; + const kernelService = { name, kref, service, systemOnly }; this.#kernelServicesByName.set(name, kernelService); this.#kernelServicesByObject.set(kref, kernelService); return kernelService; diff --git a/packages/ocap-kernel/src/vats/SubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SubclusterManager.test.ts index 45f164cde..03b6153bb 100644 --- a/packages/ocap-kernel/src/vats/SubclusterManager.test.ts +++ b/packages/ocap-kernel/src/vats/SubclusterManager.test.ts @@ -19,7 +19,9 @@ describe('SubclusterManager', () => { let mockKernelStore: Mocked; let mockKernelQueue: Mocked; let mockVatManager: Mocked; - let mockGetKernelService: (name: string) => { kref: string } | undefined; + let mockGetKernelService: ( + name: string, + ) => { kref: string; systemOnly: boolean } | undefined; let mockQueueMessage: ( target: KRef, method: string, @@ -77,7 +79,7 @@ describe('SubclusterManager', () => { mockGetKernelService = vi.fn().mockReturnValue(undefined) as unknown as ( name: string, - ) => { kref: string } | undefined; + ) => { kref: string; systemOnly: boolean } | undefined; mockQueueMessage = vi .fn() .mockResolvedValue({ body: '{"result":"ok"}', slots: [] }) as unknown as ( @@ -151,7 +153,7 @@ describe('SubclusterManager', () => { ); }); - it('includes kernel services when specified', async () => { + it('includes unrestricted kernel services when specified', async () => { const config: ClusterConfig = { bootstrap: 'testVat', vats: { @@ -161,6 +163,7 @@ describe('SubclusterManager', () => { }; (mockGetKernelService as ReturnType).mockReturnValue({ kref: 'ko-service', + systemOnly: false, }); await subclusterManager.launchSubcluster(config); @@ -172,6 +175,46 @@ describe('SubclusterManager', () => { ]); }); + it('throws when user subcluster requests a restricted service', async () => { + const config: ClusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { sourceSpec: 'test.js' }, + }, + services: ['kernelFacet'], + }; + (mockGetKernelService as ReturnType).mockReturnValue({ + kref: 'ko-service', + systemOnly: true, + }); + + await expect(subclusterManager.launchSubcluster(config)).rejects.toThrow( + "kernel service 'kernelFacet' is restricted to system subclusters", + ); + }); + + it('allows system subcluster to access restricted services', async () => { + const config: ClusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { sourceSpec: 'test.js' }, + }, + services: ['kernelFacet'], + }; + (mockGetKernelService as ReturnType).mockReturnValue({ + kref: 'ko-service', + systemOnly: true, + }); + + await subclusterManager.launchSubcluster(config, { isSystem: true }); + + expect(mockGetKernelService).toHaveBeenCalledWith('kernelFacet'); + expect(mockQueueMessage).toHaveBeenCalledWith('ko1', 'bootstrap', [ + expect.anything(), + { kernelFacet: expect.anything() }, + ]); + }); + it('throws for invalid cluster config', async () => { const invalidConfig = {} as ClusterConfig; diff --git a/packages/ocap-kernel/src/vats/SubclusterManager.ts b/packages/ocap-kernel/src/vats/SubclusterManager.ts index 468515625..2f82bebb4 100644 --- a/packages/ocap-kernel/src/vats/SubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SubclusterManager.ts @@ -22,7 +22,9 @@ type SubclusterManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; vatManager: VatManager; - getKernelService: (name: string) => { kref: string } | undefined; + getKernelService: ( + name: string, + ) => { kref: string; systemOnly: boolean } | undefined; queueMessage: ( target: KRef, method: string, @@ -45,7 +47,9 @@ export class SubclusterManager { readonly #vatManager: VatManager; /** Function to get kernel services */ - readonly #getKernelService: (name: string) => { kref: string } | undefined; + readonly #getKernelService: ( + name: string, + ) => { kref: string; systemOnly: boolean } | undefined; /** Function to queue messages */ readonly #queueMessage: ( @@ -92,17 +96,22 @@ export class SubclusterManager { * Launches a sub-cluster of vats. * * @param config - Configuration object for sub-cluster. + * @param options - Launch options. + * @param options.isSystem - Whether this is a system subcluster. System + * subclusters may access restricted kernel services. Defaults to `false`. * @returns A promise for the subcluster ID, bootstrap root kref, and * bootstrap result. */ async launchSubcluster( config: ClusterConfig, + { isSystem = false }: { isSystem?: boolean } = {}, ): Promise { await this.#kernelQueue.waitForCrank(); isClusterConfig(config) || Fail`invalid cluster config`; if (!config.vats[config.bootstrap]) { Fail`invalid bootstrap vat name ${config.bootstrap}`; } + this.#validateServices(config, isSystem); const subclusterId = this.#kernelStore.addSubcluster(config); const { rootKref, bootstrapResult } = await this.#launchVatsForSubcluster( subclusterId, @@ -204,6 +213,31 @@ export class SubclusterManager { this.#kernelStore.deleteSubcluster(subclusterId); } + /** + * Validates that all requested services exist and are accessible. + * + * @param config - The cluster configuration to validate. + * @param isSystem - Whether this is a system subcluster. + * @throws If a requested service does not exist or is system-only and the + * subcluster is not a system subcluster. + */ + #validateServices(config: ClusterConfig, isSystem: boolean): void { + if (!config.services) { + return; + } + for (const name of config.services) { + const service = this.#getKernelService(name); + if (!service) { + throw Error(`no registered kernel service '${name}'`); + } + if (service.systemOnly && !isSystem) { + throw Error( + `kernel service '${name}' is restricted to system subclusters`, + ); + } + } + } + /** * Launches all vats for a subcluster and sets up their bootstrap connections. * @@ -295,7 +329,7 @@ export class SubclusterManager { } for (const { name, config } of newConfigs) { - const result = await this.launchSubcluster(config); + const result = await this.launchSubcluster(config, { isSystem: true }); this.#systemSubclusterRoots.set(name, result.rootKref); // Persist the mapping